Living document — how selfsight works and why it's built this way. Scope lives in REQUIREMENTS.md; approach and status in PLAN.md. This doc is allowed to be verbose; it's where examples, protocol notes, and the FAQ live.
Protocol details below marked (observed) come from live WAX610 hardware. They are trusted as directionally true, but each one gets re-confirmed by a recorded transcript before Go code depends on it.
- Safety before features. These APs run real networks. Every write is preceded by a fresh backup and followed by read-back verification; fleet writes are sequential. A missing feature is annoying; a bricked AP or a dropped network is a disaster.
- Capture first. No driver code is written against an imagined API. Real device exchanges are recorded, sanitized, committed as fixtures, and the code is written to satisfy them. The fixtures double as the only public documentation of this protocol.
- API first. Every feature is a documented REST endpoint before it has UI. This keeps the server scriptable (curl, cron, integrations) and lets the frontend evolve independently.
- One artifact. A single Go binary embeds the built web UI. No database, nothing else to install. Docker is a packaging convenience, not a requirement.
- Boring technology. Stdlib-leaning Go, mainstream React. Contributors should recognize everything.
browser
│
▼
┌─────────────────────────────────────────────┐
│ selfsight (one Go binary) │
│ │
│ React SPA (static, go:embed) │
│ │ JSON │
│ ▼ │
│ REST API (net/http) │
│ │ │
│ ▼ │
│ core: inventory · desired state · drift │
│ apply engine · scheduler │
│ │ │ │
│ ▼ ▼ │
│ driver/wax (HTTPS) /data (plain │
│ │ files: backups) │
└─────┼───────────────────────────────────────┘
▼
WAX access points (local API, standalone mode)
config.yaml (mounted): inventory + desired config + credentials
Two kinds of state, deliberately separated:
- Desired state — the user's config file: which APs exist, credentials,
and what their configuration should be. It stays human-owned and
hand-editable, but selfsight can also edit it for you (add, remove, or
update a device from the UI) — that is how you adopt a discovered AP or
change its declared config without opening the file. Those edits touch only
the one device entry involved, preserving comments and everything else, and
always leave a timestamped
.bak-*of the previous file first. - Observed state — what devices actually report. Held in memory and
re-polled on demand; losing it costs nothing. selfsight writes to disk only
the config file (via the guarded editor above) and plain files under
/data(device config backups).
Drift is simply the diff between the two.
There is deliberately no database: everything selfsight knows is either re-fetchable from the devices, the user's own config file, or already a file. If a history/audit feature ever lands, that decision gets revisited — not before.
selfsight/
├── cmd/selfsight/ main: server, and the `probe`/`discover` subcommands
├── internal/
│ ├── device/ the vendor boundary: Driver + Codec contracts
│ ├── driver/wax/ session, read, write — the crown jewels
│ ├── core/ inventory, config file editing, drift
│ ├── backup/ config history store (generic) + WAX archive crypto
│ ├── discovery/ credential-free subnet sweep for APs
│ ├── capture/ recording and sanitizing real device transcripts
│ └── api/ HTTP handlers
├── web/ React app (Vite); build output embedded
├── testdata/ sanitized device transcripts (see Testing)
├── Dockerfile
└── LICENSE Apache-2.0
testdata/ is a Go toolchain convention: the compiler ignores it, so
fixtures can never leak into the shipped binary.
selfsight is built so that another brand of access point could be supported
by writing one new driver package — no server, UI, or storage code should
have to change. The boundary is internal/device, which defines two
contracts:
device.Driver— everything the server does with a live device: read status, guarded typed writes (apply an SSID, apply radio settings, delete a network, set the name — every write takes a fresh backup first and is confirmed by read-back inside the driver), backup, restore, firmware check and upgrade. All status shapes (device.Status, SSIDs, clients, firmware, results) are defined here, vendor-neutrally.device.Codec— vendor knowledge that is useful without a live device: the backup archive format and its crypto (encrypt/decrypt, validation, admin-credential fingerprint) and how to flatten an archive into the one-setting-per-line config text the history store diffs.
The WAX driver (internal/driver/wax) implements both; the server picks the
implementation in exactly one place (api.Server.newManager). Protocol
vocabulary — wlan keys, vap slots, write payload shapes — never leaves the
driver package.
Honest limits of the abstraction today: the discovery scan fingerprints are
WAX-specific, the config-history summarizer understands the WAX key
vocabulary (unknown vocabularies degrade gracefully to raw diff lines), and
the probe CLI subcommand is a WAX debugging tool. Each is contained and
none blocks a second vendor; they generalize when a second vendor actually
exists, not before.
Everything selfsight knows about talking to a WAX AP lives in
internal/driver/wax, behind a small interface, so future device classes
are new drivers rather than rewrites.
Confirmed against live WAX610 hardware (firmware V12.8.0.7) and the sanitized
transcripts under testdata/wax610/:
- Transport — HTTPS to the AP's web UI port, self-signed certificate
(verification off by default for device connections, but pinning is a
candidate feature). The cert identifies the device: subject
CN=WAX610,O=Netgear Inc.— a usable pre-auth discovery fingerprint. - Dialed by IP, always. The API refuses any request whose
Host:header is not a name the device answers to, replying status 1 with a generic "Internal error" (/LogFilereturns an "Invalid request" page). It accepts exactly two: the device's own IP, and the FQDN configured in its LAN settings, which is empty out of the box. The match is textual and strict — a zero-padded spelling of the same address (192.0.2.010for192.0.2.10) is refused, as is a different case (AP.EXAMPLE.COMforap.example.com), though a:443suffix is tolerated. Ordinary pages are served under anyHost:, so the check lives in the API handler, not the web server config; the shape is DNS-rebinding protection, and the vendor UI never trips it because it is always reached by IP. Setting an FQDN adds a name, it does not replace the IP, so the IP is the one address that always works.host:in config may therefore be a DNS name for the operator's convenience, but the driver resolves it and puts the IP in the URL, re-resolving after a transport failure so a DHCP renumber heals itself; sessions record the host they were minted against and are never replayed against another. The self-signed certificate is an independent nuisance for browsers — it carries no Subject Alternative Names — but that is a warning to click through, not a refusal, and it applies equally to the IP. - One API endpoint. Every read and write is
POST /socketCommunicationwith a JSON body. A read sends the exact config subtree with empty-string""leaves and the response echoes the shape with values filled in; you must name the exact leaves (an empty parent does not dump its subtree). The request is all-or-nothing — any unknown key fails the whole call. - Session — login establishes two secrets: a token and an
lhttpdsidcookie. The login must run inside a lighttpd session the device starts, so the driver GETs/AP_loginfirst (seedinglhttpdsidin a cookie jar), then POSTs the credentials (basicSettings.adminName/adminPasswd) to/socketCommunicationwithin that jar; the reply returns the token and the session is warm. A cold credential POST without that GET bootstrap yields a session that reads back as locked (status 100) — the bootstrap is required. Authenticated calls then send the token raw in asecurity:header with the cookie jarlhttpdsid=…; ssid=base64(token), and deliberately notimeheader: that header selects the device's "Insight-validated" path, which answers 100 on a managed AP, whereas the warm-jar path returns real data. Login is the one request that does sendtime. No browser is involved. Sessions are persisted and reused; login is re-run only when the saved one dies. That reuse is not merely an optimization — an AP grants only a small fixed number of concurrent login slots (five, observed on V12.8.0.7), and a session outlives the device's own ~45-minute bookkeeping by a wide margin. selfsight never logs out, so a slot it takes is freed by the device's expiry; logging in per call would exhaust the AP. The driver owns this dance; callers just say "logged-in client, please". - Status codes in the body, not HTTP. The envelope carries its own
status:0= success;1= bad/unknown keys in the request;100= the local API is locked;401= the session is dead (re-mint). HTTP 200 means nothing by itself.100is ambiguous and must not be read as "Insight-managed" on its own: a dead or displaced session also reads back as 100, not 401. From a reused session it means "log in again"; only a 100 from a freshly minted session means the device is genuinely managed. - Write responses lie. The AP can return success — or a transport error/timeout — for a write that did or didn't stick, and wireless writes bounce the radio (dropping the response) even on success. The driver never reports a write as successful based on the response alone: it re-reads the affected config and compares.
- Mode switching — flipping a WAX from Insight-managed to standalone (local UI: Management → Configuration → System → Basic) preserves the device configuration and does not reboot. The reverse direction resets config and reboots. selfsight documents this but never performs it; mode switching is a deliberate human act.
Every configuration write, whether from the drift "Apply" button or a future editor, goes through one pipeline:
backup → diff → write → re-read → verify
- Backup — take a fresh config backup from the device and store it.
- Diff — compute exactly which fields change; write only those.
- Write — smallest possible change, via the local API.
- Re-read — fetch the affected config back from the device.
- Verify — the change is "applied" only if the re-read shows it. Anything else is reported as a failure with both values shown.
Fleet-wide operations (apply to all, firmware upgrades) run strictly sequentially. An AP mid-write or mid-upgrade can drop its clients; doing that to every AP at once takes down the whole network, including possibly the connection driving the upgrade.
What is writable tracks exactly what the protocol work proved end to end,
nothing more: the AP name; the SSID core block (name, security, passphrase,
VLAN, hidden, enabled) — edited in place, created in a free slot, or deleted
by slot (DELETE /api/devices/{name}/ssids/{ssid}, refused for the primary
SSID and for SSIDs still declared in desired config); and per-band radio
channel and on/off. Radio transmit power, channel width, and operate mode are
deliberately not writable yet: their device value codes are unmapped, and
selfsight never writes values whose semantics aren't proven. SSID delete is
the one imperative write (an SSID absent from desired config is unmanaged,
not deleted); everything else is declarative via drift + Apply.
Backup and restore ride the AP's own mechanism (endpoints confirmed against live hardware; found in the AP's static JS, not guessed):
- Backup (read-only):
POST /LogFilewith{"method": 3, "password": "<admin password>"}— warm-jar signed; the AP re-confirms the admin password before releasing a backup — thenGET /wac510-backupfor the archive. The filename arrives inContent-Disposition(the AP names it by model/AP-name/firmware/date). The archive is encrypted by the AP, but the format is now understood and selfsight can decrypt it natively (see "Backup decryption and config history" below) — so backups are both restore targets and a readable record of past configuration. - Backups happen on change, not on a clock. A device is backed up when its
configuration actually changes: automatically before any write selfsight
makes (the hard rule — a fresh backup precedes every write), when selfsight
notices the config has changed since the last backup (including a change made
directly on the AP's own web UI), and on demand. There is no time-based
scheduler: a clock-driven backup of an unchanged config only produces a
redundant snapshot — a wasted AP login, a duplicate archive on disk, history
churn — with nothing to record. Backups still visit devices one at a time (no
fleet operation ever runs parallel) and prefix each archive with a UTC
timestamp (the device's own filename has only day granularity, so same-day
snapshots would otherwise overwrite). Implemented by
RunBackupWatcher: an initial check shortly after startup (so a change made while selfsight was down is caught promptly), then a routine check on an interval, keeping a pull only when it differs from the newest stored backup (backup.StableConfig-normalized). Both timings are configurable (server.backupCheck.startupDelay/interval, defaulting to 2m / 6h); these are check timings, not a backup schedule — an unchanged config is never stored. - Restore (DISRUPTIVE — reboots the AP):
POST /restoreSettingsasmultipart/form-data, file fieldfile, warm-jar signed plus the admin password in apasswordheader (a header, not a body field) — the AP decrypts the upload with that header password. The AP accepts exactly one.tarof at most 2 MB, and rejects an archive withouttmp/decryptedKeysinside. A restore reboots the device and drops every connected WiFi client — a far larger blast radius than a config write. - What selfsight does around a restore. Snapshots are stored decrypted, so the server re-encrypts the chosen snapshot with the current admin password just before upload — any snapshot stays restorable no matter what the password was when it was taken. Before the write it takes a fresh backup (hard rule), which doubles as the reference for the password check below.
- Restores can roll the login password back. A backup carries the AP's
login-password file (
sysconfig/shadow), and the AP's own restore script deletes the current one and copies the backup's in. So restoring a snapshot taken under a different admin password reverts the AP to that old password after the reboot. To spot this, selfsight compares the admin login stored in the config —basicSettings.adminNameplus theadminPasswdhash — between the snapshot and the fresh pre-restore backup, and refuses the restore (409,passwordReverts: true) until the caller resends withacceptPasswordRevert: true. (It does not comparesysconfig/shadowdirectly: the device re-salts that file on every reboot, so two backups of the same password never match there, which would warn on almost every restore. TheadminPasswdhash, by contrast, is byte-stable across reboots and changes only when the password actually changes.) The dashboard turns the refusal into an explicit second confirmation. After such a restore the password selfsight is configured with must be updated to the old one. - Restore verification is the session-reset check. The AP's response status is untrustworthy here (a live test saw a restore succeed while the response said otherwise), and plain reachability proves nothing — a rejected upload leaves the AP up throughout, indistinguishable from a fast reboot. What a reboot does do is kill every session on the device. So: if the pre-restore session stops authenticating, the device rebooted and the restore took; if that session still works after the wait, the restore did not take. That is the only success signal selfsight uses.
The AP encrypts its config backup, but only to protect it in transit/at rest —
the key is the device admin password, which selfsight already holds. The
format was reverse-engineered from the firmware's own file_enc (libgcrypt); a
native Go decryptor (internal/backup) was verified byte-for-byte against the
vendor binary across every available real backup, so no vendor binary or
emulator is needed at runtime. Format:
[ salt: 128 ][ IV: 16 ][ ciphertext: AES-256-CBC ][ HMAC-SHA512: 64 ]
derived = PBKDF2-HMAC-SHA512(adminPassword, salt, iter=10000, dkLen=96)
aesKey = derived[0:32] macKey = derived[32:96]
HMAC-SHA512(macKey, salt||IV||ciphertext) == trailing 64 bytes (authenticated)
The plaintext is a tar holding the device's /sysconfig/config — a flat
key value line-per-setting file (the full config, hundreds of lines,
versus the handful the live read parses) — plus sysconfig/shadow (the login
password hashes), tmp/decryptedKeys (the AP's own validity marker), and a
few optional database files. A wrong password fails the HMAC check before any
bytes are produced.
Backups are stored decrypted. The decoded snapshot is the source of truth;
encryption is only put back on for the trip to the AP (see the restore notes
above). The conversion is gated by proof: the plaintext is re-encrypted with
the archive's own salt/IV and must reproduce the AP's bytes exactly
(backup.VerifyRoundTrip) before the encrypted form is let go — a snapshot
that passes can always be turned back into a valid archive. This removes the
old trap where an archive could only ever be decrypted with the admin password
in force when it was taken. An archive that fails the check (taken under a
password we no longer have) is kept encrypted as-is and logged. On startup the
server converts archives already on disk (ConvertAndBackfill).
Config history. Because the config is line-oriented, changes over time are
just line diffs. Every backup — the automatic pre-write backup every
apply/change/restore/upgrade takes, each change selfsight detects, and manual
ones — commits the config to a local git store (<dataDir>/history, one file
per device). Endpoints:
GET …/history lists snapshots; GET …/history/diff?from=&to= returns the
change between two. Startup backfill reaches back to the oldest archive on
disk.
History is stored in full — by explicit decision. A history that dropped
data cannot serve as an audit trail, and both the store and the backups are
the operator's own local record on their own box. That makes the rule
absolute: the history repo and the backup directory must never be given a
remote, pushed, or copied off the box (data/ is git-ignored). A redaction
pass (backup.Redact, key-name blocklist with SHA-256 fingerprints) is kept
in the code for the day a shareable export is wanted.
One normalization — encrypted-at-rest secrets. The config never holds a
readable password: every credential (WiFi pre-shared keys, RADIUS/WDS/802.1x
shared secrets, radSec keys) is stored as an opaque AES-wrapped hex blob. The
AP re-wraps these on every reboot — same plaintext, fresh ciphertext — so
their stored value changes on every restart even when nothing really changed,
and a restore (which reboots) would otherwise show ~170 phantom "changes."
Since the ciphertext of a real change is indistinguishable from a re-wrap,
backup.StableConfig replaces every such blob (detected by shape — a 64+ hex
value) with a fixed «encrypted» placeholder in the history view only
(backups keep the real bytes for restore). The key stays, so a secret field
appearing or disappearing still shows; only the meaningless
ciphertext-vs-ciphertext diff is dropped. The audit signal — SSIDs, VLANs,
radios, hidden flags, names — is fully preserved. This is a display
normalization, not redaction: nothing readable was ever there to hide.
The access point itself is the sole record of its own configuration.
selfsight never keeps a competing copy: the dashboard reads the AP live
(caching the last reading on disk purely for instant display), changes made
from the UI are one-off writes to the AP (POST …/change, nothing recorded),
and disaster recovery is the stored backup snapshots. The desired: block
below is optional — declare a field and selfsight will check it and offer
to fix it (drift); declare nothing and nothing is ever compared or flagged.
One YAML file, mounted into the container (or passed by path), owns inventory and — only if you opt in — declared settings. Sketch — the real schema will grow as features land:
server:
listen: ":8080"
# No backup schedule: selfsight backs a device up when its config changes
# (before any change it makes, and when it detects one) and on demand. The
# optional backupCheck block only tunes the out-of-band check cadence.
backupCheck:
startupDelay: 2m # first check after startup (default)
interval: 6h # routine check cadence (default; minimum 1m)
devices:
- name: living-room
host: 192.0.2.20
model: WAX610
username: admin
password: "s3cret" # see Security
desired:
ssids:
- name: HomeNet
vlan: 1
security: wpa2-psk
passphrase: "also-s3cret"
radios:
2g: { channel: auto, power: full }
5g: { channel: 44, power: full }Rules:
- Unknown/omitted fields are simply not managed — selfsight only diffs and writes what the user declared. Start small, adopt gradually (or never).
- One-off UI changes and declarations must not fight:
POST …/changerefuses a field that is declared for that device (409) — the next apply would silently revert it. Change or remove the declaration instead. - The last successful status reading per device is persisted to
<dataDir>/status/<device>.jsonand served byGET …/status/cached(with its timestamp) so the dashboard paints instantly and stays informative when an AP is down. Display only — it is never compared against anything, and every successful read overwrites it. A device rename moves the file, like the backup directory. - Any value may reference an environment variable as
${VAR}, expanded when the config loads (an unset reference fails loudly at startup). This keeps secrets — device passwords, the UI auth password — out of the file, so the file can be committed to a deploy repo with the real values in a.env/ the process environment. Expansion is load-only: the UI write path edits the raw file text, so a${VAR}in an untouched device is preserved verbatim. - The file is read at startup and on demand (reload endpoint). selfsight
can also write it — adding, removing, or updating a device from the UI
(
POST/PUT/DELETE /api/devices…). Each write edits only the one device entry (comments, key order, and the other devices are preserved), validates the result before saving, and writes a timestamped.bak-*of the previous file first. Hand-editing the file still works; the write path is additive, not a replacement.
The test strategy has to work for a project whose target hardware most contributors don't own.
Capture → sanitize → commit:
- Capture real request/response exchanges against a live AP with a small capture tool (one of the project's first deliverables).
- Sanitize — a scripted pass (not manual editing) replaces MACs, serials, SSIDs, IPs, hostnames, and any credential material with plausible fakes, preserving structure byte-for-byte. Raw captures live outside this repository and are never committed — git history is permanent, so sanitization happens before the repo, not after.
- Commit the sanitized transcript under
testdata/wax610/, e.g.:
testdata/wax610/
├── login/ request + response pairs
├── status.json
├── clients.json
├── config-read.json
└── managed-mode-status-100.json
Example of what a sanitized fixture looks like (values fake, shape real):
{
"status": 0,
"clientList": [
{
"mac": "AA:BB:CC:00:11:22",
"ip": "192.0.2.10",
"ssid": "ExampleNet",
"band": "5G",
"rssi": -52
}
]
}Driver tests replay these through an in-process fake AP (an httptest
server) and assert the driver parses responses correctly and sends
correctly-shaped requests. Fast, deterministic, runs anywhere — CI never
touches hardware.
Precedents for this pattern: unpoller/unifi (Go client for UniFi's
undocumented API, tests parse sanitized real captures) and Home Assistant's
per-integration fixtures directories (captured device responses, values
genericized).
Contributing fixtures for a new model: run the capture tool against your AP, run the sanitizer, eyeball the output, open a PR with the sanitized files only. This is the main way new hardware gets supported.
Tiered, cheapest first:
- Active subnet sweep — probe candidate hosts' HTTPS port and fingerprint NETGEAR APs from pre-auth signals (TLS certificate fields, login page, MAC OUI where ARP is visible). Feeds candidates into the normal add-device API flow: found → user supplies credentials → added.
- Passive listening (mDNS/SSDP/LLDP) — only if the sweep proves insufficient; whether WAX APs announce anything useful is an open question for a capture session.
Docker caveat: TCP sweeps work from a bridge network, but ARP and
multicast do not cross the bridge. Full discovery needs
network_mode: host (documented), or users add devices by IP.
Reference deployment is Docker Compose; the same binary runs bare on any Linux/arm64/amd64 box.
services:
selfsight:
image: selfsight/selfsight
ports: ["8080:8080"]
volumes:
- ./config:/config
- selfsight-data:/data
restart: unless-stopped
volumes:
selfsight-data:/config holds the user's YAML; /data is where selfsight writes device
config backups (plain files), so they outlive the container. Deployment
automation for the maintainer's own server lives in a separate private
repository, as it contains real credentials.
- Device credentials are plaintext in the mounted config file. This is
the standard home-lab trade-off (compare Home Assistant's YAML era,
Prometheus scrape configs): the file lives on a server the user already
controls; at-rest encryption would protect against an attacker who has
already won. Documented clearly: protect the file,
chmod 600. - No secrets in this repo — enforced by the sanitizer for fixtures and by review for everything else. Real IPs/SSIDs/serials count as secrets here.
- Stored backups and history are plaintext on disk. A downloaded archive is decrypted and kept that way (0600, under the data directory), because a snapshot nobody can read is a snapshot nobody can restore or diff, and the key would have to sit on the same machine anyway. The config history keeps no readable secret: every secret-bearing value is replaced by a short hash of itself, so you can see that a passphrase changed and when, never what it is. The archives themselves do still contain WiFi passphrases — so this is an accepted risk with conditions, not an oversight: bind the server to localhost, put auth in front of it, and treat the data directory as sensitive. See SECURITY.md.
- UI/API auth — password auth (
server.auth.password) gates every API route except health and the login endpoints; the SPA stays open so it can render the login form. Sessions are random in-memory tokens in an HttpOnly cookie (24 h sliding); a server restart, or a change to the password, logs everyone out. Wrong passwords are counted per source address and that address is locked out after five in five minutes. With noauthblock the server is open — the original trusted-LAN default, and it logs a loud warning saying so — but the shipped example config enables it, because the write endpoints act on your network. Exposing selfsight beyond the LAN still wants a reverse proxy with TLS in front; behind one, the session cookie is markedSecure(detected viaX-Forwarded-Proto). - Cross-site request forgery — a request that changes something must
either be
application/jsonor carryX-Requested-With: selfsight, and itsOrigin(when the browser sends one) must be this server. Neither is something another site's page can produce, so a tab the user has open elsewhere cannot quietly reconfigure an access point using their session. Every response also carries the usual browser locks: no content-type guessing, no framing, no referrer, and a content-security policy that allows only this server's own scripts and styles. - Addresses are validated, not just used. A device host must be a plain IP or host name with an optional port; the subnet sweep only accepts private, loopback and link-local ranges. So neither can be turned into a way to make the server open connections to somewhere else, and neither can steer a cache file out of its directory (which is belt and braces — the driver's per-host caches are named by a hash of the address anyway).
- Serial pinning — each device entry can record the unit's serial number
(
serial:, captured automatically by the UI's connection test on adopt). Every write-class operation (apply, restore, firmware upgrade, name change) first compares it against the live device and refuses on mismatch — so a host that now points at different hardware (DHCP reshuffle, replaced unit) never receives another entry's config. Backups also contain WiFi passphrases: treat the data directory as sensitive. - Device TLS — AP certificates are self-signed, so classic verification is impossible; the driver instead pins each device's certificate on first use (SHA-256 of the leaf, stored next to the saved session). A later mismatch fails the connection with an error naming the pin file — delete it to re-trust after a legitimate cert change (e.g. factory reset).
Is this affiliated with NETGEAR? No. Independent project; no NETGEAR code, firmware, or cloud APIs are used. It speaks to the local web API that ships on the devices you bought.
Will it work while my APs are Insight-managed? No, and it can't: while a device is Insight-managed, the vendor locks the local configuration API (it answers with status 100). You must switch each AP to standalone mode first. On WAX610 this switch preserves the device's configuration and doesn't reboot (observed; switching back to Insight is the direction that resets config). Do it from the AP's local web UI; selfsight deliberately doesn't automate mode switching.
Why Go and React? Go compiles the whole server — web UI included — into one static binary that runs anywhere, with no runtime dependencies to install. React keeps the UI in the most widely known frontend ecosystem, which matters for contributors. Both are deliberately boring choices.
Why not just use each AP's web UI? That works for one AP. selfsight exists for fleet concerns: one dashboard, declared config with drift detection, automatic backups when the config changes, sequential firmware rollouts — the things Insight actually did for you.
Can selfsight brick my AP? The design treats this as the primary risk: writes only through the backup → diff → write → verify pipeline, minimal diffs only, sequential fleet operations, and restore-from-backup as the recovery path. Firmware flashing is inherently the riskiest operation anywhere; selfsight uses the device's own upgrade mechanism and never fleet-parallelizes it.
Which models are supported? WAX610 is the validated reference (the maintainer's production fleet). Other WAX models likely share most of the API but are unclaimed until someone contributes sanitized fixtures from real hardware. Switches and routers are a different protocol family — out of scope for v1, possible as future drivers.