feat(version-info): expose update-check state; docs

Fifth and final piece of #591's initial implementation. Extends
/api/setup/version with update_available/latest_version/
latest_release_url (nil-safe via Server.UpdateCheckResult, defaults to
Available: false when the check was never enabled). Response switched from
map[string]string to map[string]interface{} to carry the new bool field;
updated the one existing test that decoded into the old stricter type.

Documents UPDATE_CHECK_ENABLED/UPDATE_CHECK_INTERVAL in the Configuration
Options reference table, explicit that this is the only network call
AfterTouch makes beyond speaker/provider traffic when enabled, and that it
defaults off.

This closes out the initial #591 implementation per the design doc
(_/i591/design-update-check.md): UpdateCheckState persistence, the
updatecheck.Checker package, background goroutine wiring with jitter/
backoff, reusing #419's Announcements mechanism instead of a second notice
UI, and this version-info exposure. `make check` passes end to end
(including the Docker HTTP integration suite).

Refs #591
This commit is contained in:
Tobias Gesellchen
2026-08-09 01:22:03 +02:00
parent 0c343f85c7
commit afaa00e483
3 changed files with 82 additions and 33 deletions
+26 -24
View File
@@ -156,30 +156,32 @@ The service supports multiple ways to configure its behavior. When multiple sour
### Configuration Options
| Variable | Flag | Description | Default |
|------------------------------------|----------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------------------------|
| `PORT` | `--port`, `-p` | HTTP port to bind the service to | `8000` |
| `BIND_ADDR` | `--bind` | Network interface to bind to | all (ipv4 and ipv6) |
| `DATA_DIR` | `--data-dir` | Directory for persistent data | `./data` |
| `SERVER_URL` | `--server-url`, `-s` | External URL of this service | `http://<hostname>:8000` |
| `HTTPS_PORT` | `--https-port` | HTTPS port to bind the service to | `8443` |
| `HTTPS_SERVER_URL` | `--https-server-url`, `-S` | External HTTPS URL. An override: when empty it is derived from `SERVER_URL` (same host, `https`, on `HTTPS_PORT`), and can also be viewed/overridden in Settings. | derived from `SERVER_URL` |
| `PYTHON_BACKEND_URL`, `TARGET_URL` | `--target-url` | URL for Python-based service components (legacy) | `http://localhost:8001` |
| `REDACT_PROXY_LOGS` | `--redact-logs` | Redact sensitive data in proxy logs | `true` |
| `LOG_PROXY_BODY` | `--log-bodies` | Log full request/response bodies | `false` |
| `RECORD_INTERACTIONS` | `--record-interactions` | Record HTTP interactions to disk | `true` |
| `DISCOVERY_INTERVAL` | `--discovery-interval` | Device discovery interval | `5m` |
| `ENABLE_DNS_DISCOVERY` | `--dns-discovery` | Enable DNS discovery server | `false` |
| `DNS_UPSTREAM` | `--dns-upstream` | Upstream DNS server for non-Bose queries | `8.8.8.8` |
| `DNS_BIND_ADDR` | `--dns-bind` | Bind address for the DNS discovery server (standard port `:53` is required for DNS/DHCP migration) | `:53` |
| `INTERNAL_PATHS` | `--internal-paths` | Paths for internal requests to exclude from recording (e.g., `/setup/*`, `/web/*`) | `[]` |
| `DISCOVERY_DISABLED` | | Disable automated device discovery | `false` |
| `MGMT_USERNAME` | `--mgmt-username` | Username for HTTP Basic Auth on the Management API (`/api/mgmt/*`, `/mgmt/*`) — Spotify/Amazon account linking, Local Accounts | `admin` |
| `MGMT_PASSWORD` | `--mgmt-password` | Password for the same Management API Basic Auth. **Change this if AfterTouch is reachable beyond a trusted LAN** — the default is published in this doc. | `change_me!` |
| `STOCKHOLM_DIR` | `--stockholm-dir` | Path to extracted Stockholm frontend directory — enables the Stockholm UI when set | *(disabled)* |
| `MARGE_URL` | | Streaming/marge base URL used when rewriting `stockholm/json/config.json`. Defaults to `SERVER_URL`. Set to `SERVER_URL/marge` only when using a soundcork backend. | *(same as `SERVER_URL`)* |
| `MARGE_AUTH_TOKEN` | | Pre-seeds the Stockholm `margeAuthToken` state (skips the login step for the first session) | *(empty)* |
| `MARGE_ACCOUNT_ID` | | Pre-seeds the Stockholm `margeAccountID` state (used to filter device-discovery results by account) | *(empty)* |
| Variable | Flag | Description | Default |
|------------------------------------|----------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------------------------|
| `PORT` | `--port`, `-p` | HTTP port to bind the service to | `8000` |
| `BIND_ADDR` | `--bind` | Network interface to bind to | all (ipv4 and ipv6) |
| `DATA_DIR` | `--data-dir` | Directory for persistent data | `./data` |
| `SERVER_URL` | `--server-url`, `-s` | External URL of this service | `http://<hostname>:8000` |
| `HTTPS_PORT` | `--https-port` | HTTPS port to bind the service to | `8443` |
| `HTTPS_SERVER_URL` | `--https-server-url`, `-S` | External HTTPS URL. An override: when empty it is derived from `SERVER_URL` (same host, `https`, on `HTTPS_PORT`), and can also be viewed/overridden in Settings. | derived from `SERVER_URL` |
| `PYTHON_BACKEND_URL`, `TARGET_URL` | `--target-url` | URL for Python-based service components (legacy) | `http://localhost:8001` |
| `REDACT_PROXY_LOGS` | `--redact-logs` | Redact sensitive data in proxy logs | `true` |
| `LOG_PROXY_BODY` | `--log-bodies` | Log full request/response bodies | `false` |
| `RECORD_INTERACTIONS` | `--record-interactions` | Record HTTP interactions to disk | `true` |
| `DISCOVERY_INTERVAL` | `--discovery-interval` | Device discovery interval | `5m` |
| `ENABLE_DNS_DISCOVERY` | `--dns-discovery` | Enable DNS discovery server | `false` |
| `DNS_UPSTREAM` | `--dns-upstream` | Upstream DNS server for non-Bose queries | `8.8.8.8` |
| `DNS_BIND_ADDR` | `--dns-bind` | Bind address for the DNS discovery server (standard port `:53` is required for DNS/DHCP migration) | `:53` |
| `INTERNAL_PATHS` | `--internal-paths` | Paths for internal requests to exclude from recording (e.g., `/setup/*`, `/web/*`) | `[]` |
| `DISCOVERY_DISABLED` | | Disable automated device discovery | `false` |
| `UPDATE_CHECK_ENABLED` | `--update-check-enabled` | Periodically check GitHub Releases for a newer version and show a dismissible notice in the admin UI and Player when one is found. **Opt-in**: this is the only network call AfterTouch makes beyond speaker/provider traffic when enabled, so it defaults off. One unauthenticated `GET` per interval to `api.github.com`, nothing else leaves the box. | `false` |
| `UPDATE_CHECK_INTERVAL` | `--update-check-interval` | Update check interval | `24h` |
| `MGMT_USERNAME` | `--mgmt-username` | Username for HTTP Basic Auth on the Management API (`/api/mgmt/*`, `/mgmt/*`) — Spotify/Amazon account linking, Local Accounts | `admin` |
| `MGMT_PASSWORD` | `--mgmt-password` | Password for the same Management API Basic Auth. **Change this if AfterTouch is reachable beyond a trusted LAN** — the default is published in this doc. | `change_me!` |
| `STOCKHOLM_DIR` | `--stockholm-dir` | Path to extracted Stockholm frontend directory — enables the Stockholm UI when set | *(disabled)* |
| `MARGE_URL` | | Streaming/marge base URL used when rewriting `stockholm/json/config.json`. Defaults to `SERVER_URL`. Set to `SERVER_URL/marge` only when using a soundcork backend. | *(same as `SERVER_URL`)* |
| `MARGE_AUTH_TOKEN` | | Pre-seeds the Stockholm `margeAuthToken` state (skips the login step for the first session) | *(empty)* |
| `MARGE_ACCOUNT_ID` | | Pre-seeds the Stockholm `margeAccountID` state (used to filter device-discovery results by account) | *(empty)* |
### Configuration Examples
+16 -8
View File
@@ -1388,14 +1388,22 @@ func (s *Server) HandleGetVersionInfo(w http.ResponseWriter, _ *http.Request) {
releaseURL = fmt.Sprintf("%s/releases/tag/%s", repoURL, version)
}
if err := json.NewEncoder(w).Encode(map[string]string{
"version": version,
"commit": commit,
"date": date,
"repo_url": repoURL,
"release_url": releaseURL,
"commit_url": commitURL,
"data_dir": dataDir,
// Opt-in periodic update check (#591) — UpdateCheckResult is nil-safe and
// returns the zero value (Available: false) when the check was never
// enabled, which is the common case.
updateCheck := s.UpdateCheckResult()
if err := json.NewEncoder(w).Encode(map[string]interface{}{
"version": version,
"commit": commit,
"date": date,
"repo_url": repoURL,
"release_url": releaseURL,
"commit_url": commitURL,
"data_dir": dataDir,
"update_available": updateCheck.Available,
"latest_version": updateCheck.LatestVersion,
"latest_release_url": updateCheck.ReleaseURL,
}); err != nil {
http.Error(w, "Failed to encode response", http.StatusInternalServerError)
return
+40 -1
View File
@@ -468,7 +468,7 @@ func TestHandleGetVersionInfo_IncludesAbsoluteDataDir(t *testing.T) {
}
defer res.Body.Close()
var got map[string]string
var got map[string]interface{}
if err := json.NewDecoder(res.Body).Decode(&got); err != nil {
t.Fatalf("Failed to decode response: %v", err)
}
@@ -483,6 +483,45 @@ func TestHandleGetVersionInfo_IncludesAbsoluteDataDir(t *testing.T) {
}
}
// TestHandleGetVersionInfo_UpdateCheckFields verifies the #591 fields are
// present and reflect a nil-checker default (Available: false) when the
// update check was never enabled — the common case.
func TestHandleGetVersionInfo_UpdateCheckFields(t *testing.T) {
tempDir, err := os.MkdirTemp("", "version-info-updatecheck-test")
if err != nil {
t.Fatalf("Failed to create temp dir: %v", err)
}
defer os.RemoveAll(tempDir)
ds := datastore.NewDataStore(tempDir)
_ = ds.Initialize()
r, _ := setupRouter("http://127.0.0.1:8000", ds)
ts := httptest.NewServer(r)
defer ts.Close()
res, err := http.Get(ts.URL + "/setup/version")
if err != nil {
t.Fatal(err)
}
defer res.Body.Close()
var got map[string]interface{}
if err := json.NewDecoder(res.Body).Decode(&got); err != nil {
t.Fatalf("Failed to decode response: %v", err)
}
if got["update_available"] != false {
t.Errorf("Expected update_available=false by default, got %v", got["update_available"])
}
if _, ok := got["latest_version"]; !ok {
t.Error("Expected a latest_version key in the response")
}
if _, ok := got["latest_release_url"]; !ok {
t.Error("Expected a latest_release_url key in the response")
}
}
func TestMigrationAndCA(t *testing.T) {
tempDir, err := os.MkdirTemp("", "handlers-test")
if err != nil {