mirror of
https://github.com/gesellix/Bose-SoundTouch.git
synced 2026-08-18 00:26:29 +00:00
feat(settings): derive the HTTPS URL from the Target Domain, show + override in UI (#355)
The HTTPS URL AfterTouch advertises (and points speakers at for the DNS-redirect, OAuth, install-ca and cert-trust flows) was a separate, internally-tracked value: sourced only from --https-server-url / HTTPS_SERVER_URL / the settings file, defaulting to the machine hostname, and never shown or editable in the web UI. So it could silently diverge from the Target Domain (e.g. a different host, or a port-less value that fell back to 443 while the listener was on 8443 — the root of #355), with no way to see or fix it in the UI. Make it derive + show + override: - DeriveHTTPSURL resolves the effective HTTPS URL: an explicit override wins; otherwise it follows the Target Domain (same host, https, on the configured HTTPS port); an already-https Target Domain is honoured verbatim (its port is not second-guessed); empty falls back to the hostname default. So changing the Target Domain updates the HTTPS URL automatically for the common single-host case. - The persisted https_server_url is now the *override* (empty = derive). Existing installs carry their old value here, so it is preserved as an override — no silent change on upgrade; clearing it opts into derive. - The server keeps httpsServerURL as the effective value, so all consumers (cert SANs, migration, export, health) are unchanged; it is recomputed whenever the Target Domain or override changes. - Settings API returns https_server_url (effective) plus https_server_url_override; the Settings page shows the effective URL with a derived/override note and an "advanced" override field. Verified live on a clean data dir: derive from an http Target Domain, auto-follow when the Target Domain changes, explicit override, an https Target Domain kept verbatim, and override persistence across restart. Unit tests cover DeriveHTTPSURL including the already-https cases. refs #355 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
433a779998
commit
b1b3472297
@@ -518,8 +518,8 @@ func main() {
|
||||
server := handlers.NewServer(ds, sm, config.serverURL, config.redact, config.logBody, config.record)
|
||||
sm.GetDNSRunning = server.GetDNSRunning
|
||||
server.SetLogBuffer(logBuf)
|
||||
server.SetHTTPServerURL(config.httpsServerURL)
|
||||
server.SetHTTPSListenAddr(config.httpsAddr)
|
||||
server.SetHTTPSSettings(config.httpsOverride, config.httpsPort, config.httpsDefaultURL)
|
||||
server.SetExpectedHosts(config.domains)
|
||||
server.SetVersionInfo(version, commit, date, repoURL)
|
||||
server.SetDiscoverySettings(config.discoveryInterval, config.discoveryEnabled)
|
||||
@@ -683,7 +683,10 @@ type serviceConfig struct {
|
||||
dataDir string
|
||||
hostname string
|
||||
serverURL string
|
||||
httpsServerURL string
|
||||
httpsServerURL string // effective (derived or overridden)
|
||||
httpsOverride string // explicit override; "" = derive from serverURL
|
||||
httpsPort string
|
||||
httpsDefaultURL string // hostname-based fallback
|
||||
httpsAddr string
|
||||
redact bool
|
||||
logBody bool
|
||||
@@ -756,10 +759,13 @@ func loadConfig(c *cli.Context) serviceConfig {
|
||||
httpsAddr = ":" + httpsPort
|
||||
}
|
||||
|
||||
httpsServerURL := c.String("https-server-url")
|
||||
if httpsServerURL == "" {
|
||||
httpsServerURL = "https://" + hostname + ":" + httpsPort
|
||||
}
|
||||
// The HTTPS URL is an override (from the flag/env); when empty it is
|
||||
// derived from serverURL + https port so one setting (Target Domain)
|
||||
// drives both. httpsDefaultURL is the hostname-based fallback used
|
||||
// before a Target Domain is configured.
|
||||
httpsOverride := c.String("https-server-url")
|
||||
httpsDefaultURL := "https://" + hostname + ":" + httpsPort
|
||||
httpsServerURL := handlers.DeriveHTTPSURL(serverURL, httpsOverride, httpsPort, httpsDefaultURL)
|
||||
|
||||
tlsExtraHosts := c.StringSlice("tls-extra-host")
|
||||
domains := getDomains(serverURL, httpsServerURL, hostname, tlsExtraHosts)
|
||||
@@ -817,6 +823,9 @@ func loadConfig(c *cli.Context) serviceConfig {
|
||||
hostname: hostname,
|
||||
serverURL: serverURL,
|
||||
httpsServerURL: httpsServerURL,
|
||||
httpsOverride: httpsOverride,
|
||||
httpsPort: httpsPort,
|
||||
httpsDefaultURL: httpsDefaultURL,
|
||||
httpsAddr: httpsAddr,
|
||||
redact: redact,
|
||||
logBody: logBody,
|
||||
@@ -954,9 +963,12 @@ func applyPersistedSettings(ds *datastore.DataStore, config *serviceConfig) data
|
||||
config.serverURL = handlers.NormalizeServerURL(persisted.ServerURL)
|
||||
}
|
||||
|
||||
if persisted.HTTPServerURL != "" {
|
||||
config.httpsServerURL = persisted.HTTPServerURL
|
||||
}
|
||||
// persisted.HTTPServerURL is the HTTPS override (empty = derive).
|
||||
// Existing installs carry their old effective value here, so it is
|
||||
// preserved as an override; recompute the effective URL either way,
|
||||
// since serverURL may have come from the persisted settings above.
|
||||
config.httpsOverride = persisted.HTTPServerURL
|
||||
config.httpsServerURL = handlers.DeriveHTTPSURL(config.serverURL, config.httpsOverride, config.httpsPort, config.httpsDefaultURL)
|
||||
|
||||
config.discoveryEnabled = persisted.DiscoveryEnabled
|
||||
if persisted.DiscoveryInterval != "" {
|
||||
@@ -1076,7 +1088,7 @@ func applyPersistedMusicServiceCredentials(config *serviceConfig, persisted data
|
||||
func createDefaultSettings(ds *datastore.DataStore, config serviceConfig) datastore.Settings {
|
||||
settings := datastore.Settings{
|
||||
ServerURL: config.serverURL,
|
||||
HTTPServerURL: config.httpsServerURL,
|
||||
HTTPServerURL: config.httpsOverride,
|
||||
RedactLogs: config.redact,
|
||||
LogBodies: config.logBody,
|
||||
RecordInteractions: config.record,
|
||||
|
||||
@@ -162,6 +162,7 @@ func (s *Server) HandleGetSettings(w http.ResponseWriter, _ *http.Request) {
|
||||
|
||||
s.mu.RLock()
|
||||
serverURL, httpsServerURL := s.serverURL, s.httpsServerURL
|
||||
httpsOverride := s.httpsOverride
|
||||
discoveryInterval := s.discoveryInterval.String()
|
||||
discoveryEnabled := s.discoveryEnabled
|
||||
dnsEnabled := s.dnsEnabled
|
||||
@@ -230,6 +231,7 @@ func (s *Server) HandleGetSettings(w http.ResponseWriter, _ *http.Request) {
|
||||
"server_url_resolved_ip": serverURLResolvedIP,
|
||||
"server_url_resolve_error": serverURLResolveError,
|
||||
"https_server_url": httpsServerURL,
|
||||
"https_server_url_override": httpsOverride,
|
||||
"https_listener_port": httpsListenerPort,
|
||||
"https_443_check_skipped": probe443.Skipped,
|
||||
"https_443_not_applicable": probe443.NotApplicable,
|
||||
@@ -278,28 +280,29 @@ func (s *Server) HandleGetSettings(w http.ResponseWriter, _ *http.Request) {
|
||||
// HandleUpdateSettings updates the service settings.
|
||||
func (s *Server) HandleUpdateSettings(w http.ResponseWriter, r *http.Request) {
|
||||
var settings struct {
|
||||
ServerURL string `json:"server_url"`
|
||||
DiscoveryInterval string `json:"discovery_interval"`
|
||||
DiscoveryEnabled bool `json:"discovery_enabled"`
|
||||
DNSEnabled bool `json:"dns_enabled"`
|
||||
DNSUpstream string `json:"dns_upstream"`
|
||||
DNSBindAddr string `json:"dns_bind_addr"`
|
||||
InternalPaths []string `json:"internal_paths"`
|
||||
Shortcuts map[string]int `json:"shortcuts"`
|
||||
SpotifyClientID string `json:"spotify_client_id"`
|
||||
SpotifyClientSecret string `json:"spotify_client_secret"`
|
||||
SpotifyRedirectURI string `json:"spotify_redirect_uri"`
|
||||
AmazonClientID string `json:"amazon_client_id"`
|
||||
AmazonClientSecret string `json:"amazon_client_secret"`
|
||||
AmazonRedirectURI string `json:"amazon_redirect_uri"`
|
||||
TTSProvider string `json:"tts_provider"`
|
||||
TTSGoogleAPIKey string `json:"tts_google_api_key"`
|
||||
TTSAppKey string `json:"tts_app_key"`
|
||||
TTSLanguage string `json:"tts_language"`
|
||||
TTSVoice string `json:"tts_voice"`
|
||||
TTSVolume int `json:"tts_volume"`
|
||||
TLSExtraHosts *[]string `json:"tls_extra_hosts"`
|
||||
DefaultLanding string `json:"default_landing"`
|
||||
ServerURL string `json:"server_url"`
|
||||
HTTPSServerURLOverride *string `json:"https_server_url_override"`
|
||||
DiscoveryInterval string `json:"discovery_interval"`
|
||||
DiscoveryEnabled bool `json:"discovery_enabled"`
|
||||
DNSEnabled bool `json:"dns_enabled"`
|
||||
DNSUpstream string `json:"dns_upstream"`
|
||||
DNSBindAddr string `json:"dns_bind_addr"`
|
||||
InternalPaths []string `json:"internal_paths"`
|
||||
Shortcuts map[string]int `json:"shortcuts"`
|
||||
SpotifyClientID string `json:"spotify_client_id"`
|
||||
SpotifyClientSecret string `json:"spotify_client_secret"`
|
||||
SpotifyRedirectURI string `json:"spotify_redirect_uri"`
|
||||
AmazonClientID string `json:"amazon_client_id"`
|
||||
AmazonClientSecret string `json:"amazon_client_secret"`
|
||||
AmazonRedirectURI string `json:"amazon_redirect_uri"`
|
||||
TTSProvider string `json:"tts_provider"`
|
||||
TTSGoogleAPIKey string `json:"tts_google_api_key"`
|
||||
TTSAppKey string `json:"tts_app_key"`
|
||||
TTSLanguage string `json:"tts_language"`
|
||||
TTSVoice string `json:"tts_voice"`
|
||||
TTSVolume int `json:"tts_volume"`
|
||||
TLSExtraHosts *[]string `json:"tls_extra_hosts"`
|
||||
DefaultLanding string `json:"default_landing"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&settings); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
@@ -343,6 +346,9 @@ func (s *Server) HandleUpdateSettings(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
s.mu.Lock()
|
||||
s.serverURL = settings.ServerURL
|
||||
// nil override = "field omitted, preserve"; recompute regardless, since
|
||||
// the Target Domain (which the derived URL follows) may have changed.
|
||||
s.applyHTTPSOverrideLocked(settings.HTTPSServerURLOverride)
|
||||
|
||||
s.discoveryEnabled = settings.DiscoveryEnabled
|
||||
if settings.DiscoveryInterval != "" {
|
||||
@@ -397,7 +403,9 @@ func (s *Server) HandleUpdateSettings(w http.ResponseWriter, r *http.Request) {
|
||||
currentRedact := s.redactLogs
|
||||
currentLogBody := s.logBodies
|
||||
currentRecord := s.recordEnabled
|
||||
currentHTTPS := s.httpsServerURL
|
||||
// Persist the override (empty = derive), not the effective URL, so the
|
||||
// HTTPS URL keeps following the Target Domain across restarts.
|
||||
currentHTTPS := s.httpsOverride
|
||||
|
||||
// Resolve TLS extra hosts: nil pointer means "field omitted, preserve existing";
|
||||
// non-nil (even empty) means "replace with this list".
|
||||
@@ -1008,14 +1016,15 @@ func (s *Server) HandleUpdateLoggingSettings(w http.ResponseWriter, r *http.Requ
|
||||
|
||||
// Persist to datastore
|
||||
// Access fields directly since we already hold the lock
|
||||
serverURL, httpsServerURL := s.serverURL, s.httpsServerURL
|
||||
// Persist the HTTPS override (empty = derive), not the effective URL.
|
||||
serverURL, httpsOverride := s.serverURL, s.httpsOverride
|
||||
discoveryInterval := s.discoveryInterval.String()
|
||||
discoveryEnabled := s.discoveryEnabled
|
||||
|
||||
log.Printf("Saving updated proxy settings to %s/settings.json", s.ds.DataDir)
|
||||
err := s.ds.SaveSettings(datastore.Settings{
|
||||
ServerURL: serverURL,
|
||||
HTTPServerURL: httpsServerURL,
|
||||
HTTPServerURL: httpsOverride,
|
||||
RedactLogs: s.redactLogs,
|
||||
LogBodies: s.logBodies,
|
||||
RecordInteractions: s.recordEnabled,
|
||||
|
||||
@@ -130,6 +130,42 @@ func schemeOf(s string) string {
|
||||
return strings.ToLower(u.Scheme)
|
||||
}
|
||||
|
||||
// DeriveHTTPSURL resolves the effective HTTPS URL AfterTouch advertises.
|
||||
//
|
||||
// The rules, in order:
|
||||
// - a non-empty override wins verbatim (set via --https-server-url /
|
||||
// HTTPS_SERVER_URL or the "advanced" field in the web UI; needed for
|
||||
// reverse-proxy setups where the public HTTPS endpoint differs).
|
||||
// - if the Target Domain is itself an https:// URL, use it as-is: the
|
||||
// operator has already named an HTTPS endpoint (host and, if given,
|
||||
// port), so we must not second-guess its port.
|
||||
// - otherwise derive from the http:// serverURL: same host, https
|
||||
// scheme, on httpsPort. This keeps the common single-host case to one
|
||||
// setting — change the Target Domain and the HTTPS URL follows.
|
||||
// - if serverURL has no usable host (e.g. not configured yet), fall
|
||||
// back to the startup default (hostname-based).
|
||||
func DeriveHTTPSURL(serverURL, override, httpsPort, fallback string) string {
|
||||
if strings.TrimSpace(override) != "" {
|
||||
return override
|
||||
}
|
||||
|
||||
if u, err := url.Parse(serverURL); err == nil && u.Hostname() != "" {
|
||||
// Target Domain already points at HTTPS: honour it verbatim
|
||||
// (scheme + host + whatever port the operator specified, or none).
|
||||
if strings.EqualFold(u.Scheme, "https") {
|
||||
return "https://" + u.Host
|
||||
}
|
||||
|
||||
if httpsPort != "" {
|
||||
return "https://" + net.JoinHostPort(u.Hostname(), httpsPort)
|
||||
}
|
||||
|
||||
return "https://" + u.Hostname()
|
||||
}
|
||||
|
||||
return fallback
|
||||
}
|
||||
|
||||
// PortFromHTTPSServerURL extracts the numeric port from httpsServerURL. It
|
||||
// returns 0 if the URL is empty, malformed, or has no explicit port — in
|
||||
// that case the caller cannot make a determination about :443 and should
|
||||
|
||||
@@ -137,6 +137,37 @@ func TestPortFromHTTPSServerURL(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeriveHTTPSURL(t *testing.T) {
|
||||
const fallback = "https://myhost:8443"
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
serverURL, override, httpsPort string
|
||||
want string
|
||||
}{
|
||||
{"override wins verbatim", "http://192.0.2.10:8000", "https://proxy.example:443", "8443", "https://proxy.example:443"},
|
||||
{"override wins over https serverURL", "https://192.0.2.10:9000", "https://proxy.example", "8443", "https://proxy.example"},
|
||||
{"http derives to https on https port", "http://192.0.2.10:8000", "", "8443", "https://192.0.2.10:8443"},
|
||||
{"http host without port still derives on https port", "http://192.0.2.10", "", "8443", "https://192.0.2.10:8443"},
|
||||
// The Target Domain is already HTTPS: honour it verbatim, do not
|
||||
// re-impose the configured https port (issue #355 follow-up).
|
||||
{"https serverURL with port kept as-is", "https://192.0.2.10:8443", "", "8443", "https://192.0.2.10:8443"},
|
||||
{"https serverURL with custom port kept as-is", "https://192.0.2.10:9000", "", "8443", "https://192.0.2.10:9000"},
|
||||
{"https serverURL without port kept as-is", "https://192.0.2.10", "", "8443", "https://192.0.2.10"},
|
||||
{"empty serverURL falls back", "", "", "8443", fallback},
|
||||
{"unparseable serverURL falls back", "://nope", "", "8443", fallback},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
got := DeriveHTTPSURL(tc.serverURL, tc.override, tc.httpsPort, fallback)
|
||||
if got != tc.want {
|
||||
t.Errorf("DeriveHTTPSURL(%q, %q, %q) = %q, want %q", tc.serverURL, tc.override, tc.httpsPort, got, tc.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestFormatPreflightGuidance_SkippedAndAllOK(t *testing.T) {
|
||||
if FormatPreflightGuidance(443, Probe443Result{Skipped: true}) != "" {
|
||||
t.Errorf("expected empty guidance when skipped")
|
||||
|
||||
@@ -40,7 +40,10 @@ type Server struct {
|
||||
sm *setup.Manager
|
||||
mu sync.RWMutex
|
||||
serverURL string
|
||||
httpsServerURL string
|
||||
httpsServerURL string // effective (derived or overridden) HTTPS URL
|
||||
httpsOverride string // explicit HTTPS URL override; "" means derive from serverURL
|
||||
httpsPort string // configured HTTPS port, used when deriving
|
||||
httpsDefaultURL string // startup hostname-based fallback when serverURL has no host
|
||||
httpsListenAddr string
|
||||
discovering bool
|
||||
redactLogs bool
|
||||
@@ -796,6 +799,10 @@ func (s *Server) GetDiscoverySettings() (time.Duration, bool) {
|
||||
}
|
||||
|
||||
// SetHTTPServerURL sets the external HTTPS URL of the service.
|
||||
//
|
||||
// Deprecated: prefer SetHTTPSSettings, which tracks the override vs the
|
||||
// derived value so the effective URL follows the Target Domain. Kept for
|
||||
// callers that set the effective URL directly.
|
||||
func (s *Server) SetHTTPServerURL(url string) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
@@ -803,6 +810,46 @@ func (s *Server) SetHTTPServerURL(url string) {
|
||||
s.httpsServerURL = url
|
||||
}
|
||||
|
||||
// SetHTTPSSettings records the HTTPS URL override (empty = derive from
|
||||
// the Target Domain), the configured HTTPS port, and the startup
|
||||
// hostname-based fallback, then recomputes the effective HTTPS URL.
|
||||
func (s *Server) SetHTTPSSettings(override, httpsPort, defaultURL string) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
s.httpsOverride = strings.TrimSpace(override)
|
||||
s.httpsPort = httpsPort
|
||||
s.httpsDefaultURL = defaultURL
|
||||
s.recomputeHTTPSURLLocked()
|
||||
}
|
||||
|
||||
// recomputeHTTPSURLLocked refreshes the effective HTTPS URL from the
|
||||
// current serverURL + override + port. Callers must hold s.mu.
|
||||
func (s *Server) recomputeHTTPSURLLocked() {
|
||||
s.httpsServerURL = DeriveHTTPSURL(s.serverURL, s.httpsOverride, s.httpsPort, s.httpsDefaultURL)
|
||||
}
|
||||
|
||||
// applyHTTPSOverrideLocked sets the HTTPS override from an optional
|
||||
// request value (nil = preserve the current one, non-nil replaces it,
|
||||
// empty re-enables deriving) and recomputes the effective URL. Callers
|
||||
// must hold s.mu.
|
||||
func (s *Server) applyHTTPSOverrideLocked(override *string) {
|
||||
if override != nil {
|
||||
s.httpsOverride = strings.TrimSpace(*override)
|
||||
}
|
||||
|
||||
s.recomputeHTTPSURLLocked()
|
||||
}
|
||||
|
||||
// HTTPSOverride returns the explicit HTTPS URL override, or "" when the
|
||||
// effective URL is derived from the Target Domain.
|
||||
func (s *Server) HTTPSOverride() string {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
|
||||
return s.httpsOverride
|
||||
}
|
||||
|
||||
// SetHTTPSListenAddr records the address the HTTPS listener is bound
|
||||
// to (e.g. ":8443"). The cert-chain health check uses its port to
|
||||
// detect an advertised-URL/listener port mismatch (issue #355).
|
||||
|
||||
@@ -183,6 +183,27 @@
|
||||
<div id="target-domain-resolved" style="font-size: 0.85em; margin-top: 4px; min-height: 1.2em"></div>
|
||||
<div id="https-443-status" style="font-size: 0.85em; margin-top: 4px; min-height: 1.2em"></div>
|
||||
</div>
|
||||
<div style="margin-bottom: 20px">
|
||||
<label>HTTPS URL:</label>
|
||||
<code id="https-url-effective">—</code>
|
||||
<span id="https-url-effective-note" style="font-size: 0.8em; color: #666"></span>
|
||||
<div style="margin-top: 6px">
|
||||
<span class="info-toggle" onclick="toggleInfo('https-url-override-info')">Advanced: override HTTPS URL ⓘ</span>
|
||||
<div id="https-url-override-info" class="info-details">
|
||||
<p style="font-size: 0.85em; color: #666; margin: 4px 0">
|
||||
AfterTouch also serves an HTTPS endpoint (used for the
|
||||
DNS-based redirect, music-service login, and certificate
|
||||
trust). By default it follows the Target Domain above
|
||||
(same host, on the HTTPS port), so you normally don't
|
||||
set anything here. Provide an override only when a
|
||||
reverse proxy serves HTTPS on a different host or port.
|
||||
Leave it empty to go back to deriving it automatically.
|
||||
Takes effect after saving.
|
||||
</p>
|
||||
<input type="text" id="https-url-override" placeholder="https://host:8443 (empty = derive)" style="width: 300px"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style="margin-bottom: 20px">
|
||||
<label for="default-landing">Landing page (<code>/</code>):</label>
|
||||
<select id="default-landing" style="margin-left: 4px">
|
||||
|
||||
@@ -250,6 +250,20 @@ async function fetchSettings() {
|
||||
if (settings.server_url) {
|
||||
document.getElementById("target-domain").value = settings.server_url;
|
||||
}
|
||||
const httpsEff = document.getElementById("https-url-effective");
|
||||
if (httpsEff) {
|
||||
httpsEff.textContent = settings.https_server_url || "—";
|
||||
}
|
||||
const httpsOverrideInput = document.getElementById("https-url-override");
|
||||
if (httpsOverrideInput) {
|
||||
httpsOverrideInput.value = settings.https_server_url_override || "";
|
||||
}
|
||||
const httpsEffNote = document.getElementById("https-url-effective-note");
|
||||
if (httpsEffNote) {
|
||||
httpsEffNote.textContent = settings.https_server_url_override
|
||||
? "(override)"
|
||||
: "(derived from Target Domain)";
|
||||
}
|
||||
const resolved = document.getElementById("target-domain-resolved");
|
||||
if (resolved) {
|
||||
if (settings.server_url_resolved_ip) {
|
||||
@@ -461,8 +475,10 @@ async function updateLoggingSettings() {
|
||||
}
|
||||
|
||||
async function updateSettings() {
|
||||
const httpsOverrideEl = document.getElementById("https-url-override");
|
||||
const settings = {
|
||||
server_url: document.getElementById("target-domain").value,
|
||||
https_server_url_override: httpsOverrideEl ? httpsOverrideEl.value.trim() : "",
|
||||
default_landing: document.getElementById("default-landing").value,
|
||||
discovery_interval: document.getElementById("discovery-interval").value,
|
||||
discovery_enabled: document.getElementById("discovery-enabled").checked,
|
||||
|
||||
Reference in New Issue
Block a user