diff --git a/cmd/server/config.go b/cmd/server/config.go
index 4295517b..4b7a6cfc 100644
--- a/cmd/server/config.go
+++ b/cmd/server/config.go
@@ -63,7 +63,8 @@ type Config struct {
Roles map[string]interface{} `json:"roles"`
HealthThresholds *HealthThresholds `json:"healthThresholds"`
- Tiles map[string]interface{} `json:"tiles"`
+ Map map[string]interface{} `json:"map"`
+ Tiles map[string]interface{} `json:"tiles"` // deprecated
SnrThresholds map[string]interface{} `json:"snrThresholds"`
DistThresholds map[string]interface{} `json:"distThresholds"`
MaxHopDist *float64 `json:"maxHopDist"`
@@ -99,10 +100,7 @@ type Config struct {
DebugAffinity bool `json:"debugAffinity,omitempty"`
// MapDarkTileProvider selects the default dark-mode basemap provider for
- // new visitors. The client may override per-browser via the customizer
- // (persisted to localStorage). Allowed values: "carto-dark" (default),
- // "esri-darkgray-labels", "voyager-inverted", "positron-inverted". See
- // public/map-tile-providers.js for the registry. #1420.
+ // new visitors. Deprecated: use Map.Tiles.DarkDefault instead.
MapDarkTileProvider string `json:"mapDarkTileProvider,omitempty"`
// ObserverBlacklist is a list of observer public keys to exclude from API
@@ -363,14 +361,48 @@ func LoadConfig(baseDirs ...string) (*Config, error) {
continue
}
cfg.NormalizeTimestampConfig()
+ cfg.migrateDeprecatedConfig()
applyCORSEnv(cfg)
return cfg, nil
}
cfg.NormalizeTimestampConfig()
+ cfg.migrateDeprecatedConfig()
applyCORSEnv(cfg)
return cfg, nil // defaults
}
+func (c *Config) migrateDeprecatedConfig() {
+ migrated := false
+ if c.Map == nil {
+ c.Map = make(map[string]interface{})
+ }
+ if c.Map["tiles"] == nil {
+ c.Map["tiles"] = make(map[string]interface{})
+ }
+ tilesMap, ok := c.Map["tiles"].(map[string]interface{})
+ if !ok {
+ return
+ }
+
+ if c.MapDarkTileProvider != "" {
+ if tilesMap["darkDefault"] == nil {
+ tilesMap["darkDefault"] = c.MapDarkTileProvider
+ }
+ migrated = true
+ }
+ if len(c.Tiles) > 0 {
+ for k, v := range c.Tiles {
+ if tilesMap[k] == nil {
+ tilesMap[k] = v
+ }
+ }
+ migrated = true
+ }
+ if migrated {
+ fmt.Fprintf(os.Stderr, "[deprecated] Top-level 'mapDarkTileProvider' and 'tiles' keys in config.json are deprecated and will be ignored in v3.5.0 (see #1165). Please move them into 'map': { 'tiles': { ... } }.\n")
+ }
+}
+
func LoadTheme(baseDirs ...string) *ThemeFile {
if len(baseDirs) == 0 {
baseDirs = []string{"."}
diff --git a/cmd/server/routes.go b/cmd/server/routes.go
index d1fef5be..9d41f03e 100644
--- a/cmd/server/routes.go
+++ b/cmd/server/routes.go
@@ -396,7 +396,7 @@ func (s *Server) handleConfigClient(w http.ResponseWriter, r *http.Request) {
writeJSON(w, ClientConfigResponse{
Roles: s.cfg.Roles,
HealthThresholds: s.cfg.GetHealthThresholds().ToClientMs(),
- Tiles: s.cfg.Tiles,
+ Map: s.cfg.Map,
SnrThresholds: s.cfg.SnrThresholds,
DistThresholds: s.cfg.DistThresholds,
MaxHopDist: s.cfg.MaxHopDist,
@@ -409,6 +409,7 @@ func (s *Server) handleConfigClient(w http.ResponseWriter, r *http.Request) {
Timestamps: s.cfg.GetTimestampConfig(),
DebugAffinity: s.cfg.DebugAffinity,
MapDarkTileProvider: s.cfg.MapDarkTileProvider,
+ Tiles: s.cfg.Tiles,
})
}
diff --git a/cmd/server/types.go b/cmd/server/types.go
index 630e773b..42b19c19 100644
--- a/cmd/server/types.go
+++ b/cmd/server/types.go
@@ -992,7 +992,8 @@ type MapConfigResponse struct {
type ClientConfigResponse struct {
Roles interface{} `json:"roles"`
HealthThresholds interface{} `json:"healthThresholds"`
- Tiles interface{} `json:"tiles"`
+ Map interface{} `json:"map"`
+ Tiles interface{} `json:"tiles,omitempty"` // deprecated
SnrThresholds interface{} `json:"snrThresholds"`
DistThresholds interface{} `json:"distThresholds"`
MaxHopDist interface{} `json:"maxHopDist"`
@@ -1004,9 +1005,7 @@ type ClientConfigResponse struct {
PropagationBufferMs float64 `json:"propagationBufferMs"`
Timestamps TimestampConfig `json:"timestamps"`
DebugAffinity bool `json:"debugAffinity,omitempty"`
- // #1420 — server default for dark-tile provider picker. Client uses this
- // as the fallback when no localStorage override is set.
- MapDarkTileProvider string `json:"mapDarkTileProvider,omitempty"`
+ MapDarkTileProvider string `json:"mapDarkTileProvider,omitempty"` // deprecated. TODO: remove after v3.5.0
}
// ─── IATA Coords ───────────────────────────────────────────────────────────────
diff --git a/config.example.json b/config.example.json
index 06dc9dd0..570f5566 100644
--- a/config.example.json
+++ b/config.example.json
@@ -55,8 +55,30 @@
"opacity": 1,
"_comment": "#1488/#1506 — outline around each map marker (live + map pages). Defaults restored to v3.7.2 visual (solid white, 2px). 'color' accepts any CSS color (hex, rgb, rgba); use rgba() or drop 'opacity' below ~0.5 to soften the outline when hundreds of nodes feel overwhelming. 'width' is the SVG stroke width (0 hides the outline entirely). Operators can override per-browser via the in-app Theme Customizer (Colors tab → Marker Stroke)."
},
- "mapDarkTileProvider": "carto-dark",
- "_comment_mapDarkTileProvider": "Default dark-mode basemap provider. Allowed: 'carto-dark' (Carto dark_all — default), 'esri-darkgray-labels' (Esri Dark Gray Canvas + reference labels), 'voyager-inverted' (Carto Voyager with CSS invert filter), 'positron-inverted' (Carto Positron with CSS invert filter). Light mode is unaffected. Users can override per-browser via the in-app customizer (persisted to localStorage). #1420.",
+ "map": {
+ "tiles": {
+ "darkDefault": "carto-dark",
+ "lightDefault": "carto-light",
+ "providers": {
+ "_comment_carto": "Carto is the default free-tier provider. Optional: specify 'domain' for Carto enterprise (e.g. 'mycompany' for 'https://{s}.mycompany.cartocdn.com').",
+ "carto": {
+ "enabled": true,
+ "domain": ""
+ },
+ "_comment_osm": "OSM providers: 'mapbox', 'thunderforest', 'maptiler'. WARNING: Tokens are sent to the browser. Apply origin/referrer restrictions in your provider dashboard.",
+ "osm": {
+ "enabled": false,
+ "provider": "",
+ "token": ""
+ },
+ "_comment_stamen": "Stamen (hosted by Stadia). WARNING: Tokens are sent to the browser. Apply origin/referrer restrictions.",
+ "stamen": {
+ "enabled": false,
+ "token": ""
+ }
+ }
+ }
+ },
"home": {
"heroTitle": "CoreScope",
"heroSubtitle": "Find your nodes to start monitoring them.",
diff --git a/docs/deployment.md b/docs/deployment.md
index 2d1bdb61..a7c15a0c 100644
--- a/docs/deployment.md
+++ b/docs/deployment.md
@@ -182,6 +182,11 @@ See `config.example.json` in the repository for all available options including:
- Region filters
- Retention policies
- Geo-filtering
+- Map tile providers (OSM, Stamen, Carto, etc.)
+
+### Map Tile Providers
+
+Map tile providers are enabled and configured via the `config.json` file. You can provide your custom API credentials (e.g. `osm_url`, `stamen_api_key`, `mapbox_api_key`) to activate external tile services. Once configured on the server, users can select their preferred tile provider from the Customizer UI on the client, and their choice will be persisted automatically.
---
diff --git a/public/customize-v2.js b/public/customize-v2.js
index a49d2208..7eb83107 100644
--- a/public/customize-v2.js
+++ b/public/customize-v2.js
@@ -1390,7 +1390,7 @@
'
Re-show first-visit gesture discoverability hints (swipe rows, swipe tabs, edge-swipe drawer, pull-to-refresh).
' +
'' +
_renderChannelsShowEncryptedToggle() +
- _renderDarkTileProviderSelector() +
+ _renderTileProviderSelector() +
'';
}
@@ -1415,24 +1415,50 @@
// ── #1420 Dark-tile provider selector ──
// Persists per-browser via MC_setDarkTileProvider; map.js / live.js
// listen for `mc-tile-provider-changed` and swap tiles live.
- function _renderDarkTileProviderSelector() {
+
+ function _renderTileProviderSelector() {
var reg = (typeof window !== 'undefined') && window.MC_TILE_PROVIDERS;
if (!reg) return '';
- var active = (typeof window.MC_getDarkTileProvider === 'function') ? window.MC_getDarkTileProvider() : 'carto-dark';
- var ids = ['carto-dark', 'esri-darkgray-labels', 'voyager-inverted', 'positron-inverted'];
- var options = ids.filter(function (id) { return reg[id]; }).map(function (id) {
- var label = reg[id].label || id;
- var sel = id === active ? ' selected' : '';
- return '';
- }).join('');
- return 'Dark Map Tiles
' +
- 'Choose the dark-mode basemap. Light mode is unaffected. Inverted variants apply a CSS filter for higher contrast.
' +
- '' +
- '