ultra_settings 2.6.1 → 2.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ec11cc8661b191dbb102deec0798f47ef11495feb8675f2f5b83e3e5d752855f
4
- data.tar.gz: e3f729687eaad31be948aa8f7b4818f25fb842cec8261058aa1a9f62616639c6
3
+ metadata.gz: 846f2c54ef9d4019e4f97ad02dcc962813a29fccc6128fe1e18ef9f263c01c2e
4
+ data.tar.gz: 7d28f71ae610362fd8e2c4e0ffe2ae885d7b9f270fc0e942f41fd7e8daff18fc
5
5
  SHA512:
6
- metadata.gz: 455632dc476fc7ed73a70d784d97de612db55c1c2650e31b0896727f9747c8e4ddc3a2bfcdf9c2b190180c4f3d157aca427b75abf68c6e5f312b1251d9f24fc5
7
- data.tar.gz: 83e7686584d1ea62747d693c17f9f43908d70a5f5b45541d9f129b2e33e92521b0ce0c87d4c8ec10e3416afd186fefee93f281a6bfa0871c8b56e741d1fc44f6
6
+ metadata.gz: 8829e070b1f37474fc6673804d219d1485b2cb36c5d81f9dcd4b88ff6623bd1f99a5a135adedf4cc2a2f1865ac27d74b8f405b01896aabc3da2b5e41226333f3
7
+ data.tar.gz: 0e906fddfea5764d653beb41c99d534c30b44974a83b8f7e3de1d76dd1656e367fe160743f4ac65ec422bf0e1c7d0c35b28ff2d5e3ce4252bf70d53d0f367709
data/CHANGELOG.md CHANGED
@@ -4,17 +4,38 @@ All notable changes to this project will be documented in this file.
4
4
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
5
5
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
+ ## 2.8.0
8
+
9
+ ### Added
10
+
11
+ - Added support for setting a description on configuration classes. The class description can serve to document the purpose of the configuration and will be shown in the web UI.
12
+ - Improved menu for selecting configuration classes in the web UI by adding a search box to filter the list of configurations.
13
+
14
+ ### Fixed
15
+
16
+ - Fixed filtering of data sources for configuration classes in the web UI.
17
+ - The YAML source will not be shown if the configuration file is set to nil or false.
18
+ - The runtime settings source will not be shown if the runtime settings engine is not set up.
19
+ - Fields that override the class default for a data source will now correctly show that source.
20
+
21
+ ## 2.7.0
22
+
23
+ ### Added
24
+
25
+ - Added new setting to indicate if the runtime settings engine is secure. If the engine is marked as not secure, then runtime settings will be disabled for all fields marked as secret. This protects sensitive information from being exposed in the web UI or through the API. The default value is `true` to maintain backwards compatibility.
26
+
7
27
  ## 2.6.1
8
28
 
9
29
  ### Added
10
30
 
11
31
  - Show icons on the web UI that open a dialog with the current value for each data source.
32
+ - Added support for passing the field description in `UltraSettings.runtime_settings_url` using the `${description}` placeholder in the URL.
12
33
 
13
34
  ## 2.6.0
14
35
 
15
36
  ### Added
16
37
 
17
- - Added support for passing the type in `UltraSettings.runtime_settings_url` as `${type}` in the URL.
38
+ - Added support for passing the type in `UltraSettings.runtime_settings_url` using the `${type}` placeholder in the URL.
18
39
 
19
40
  ### Changed
20
41
 
data/README.md CHANGED
@@ -81,7 +81,6 @@ class MyServiceConfiguration < UltraSettings::Configuration
81
81
  field :auth_token,
82
82
  type: :string,
83
83
  env_var: "MY_SERVICE_TOKEN",
84
- runtime_setting: false,
85
84
  yaml_key: false,
86
85
  description: "Bearer token for accessing the service",
87
86
  secret: true
@@ -114,7 +113,7 @@ You can customize the behavior of each field using various options:
114
113
 
115
114
  - `:default_if` - Provides a condition for when the default should be used. This should be a Proc or the name of a method within the class. Useful for ensuring values meet specific constraints. This can provide protection from misconfiguration that can break the application. In the above example, the default value for `timeout` will be used if the value is less than or equal to 0.
116
115
 
117
- - `:secret` - Marks the field as secret. Secret fields are not displayed in the web UI. By default, all fields are considered secret to avoid accidentally exposing sensitive values. You can change this default behavior by setting `fields_secret_by_default` to `false` either globally or per configuration.
116
+ - `:secret` - Marks the field as secret. Secret fields are not displayed in the web UI. By default, all fields are considered secret to avoid accidentally exposing sensitive values. You can change this default behavior by setting `fields_secret_by_default` to `false` either globally or per configuration. Additionaly, if you set `UltraSettings.runtime_settings_secure` to false, then runtime settings will be disabled on secret fields.
118
117
 
119
118
  - `:env_var` - Overrides the environment variable name used to populate the field. This is useful if the variable name does not follow the conventional pattern. Set this to `false` to disable loading the field from an environment variable.
120
119
 
@@ -172,6 +171,9 @@ UltraSettings.runtime_settings = RedisRuntimeSettings.new
172
171
 
173
172
  The runtime settings implementation may also define an `array` method that takes a single parameter to return an array value. If this method is not implemented, then array values must be returned as single line CSV strings.
174
173
 
174
+ > [!TIP]
175
+ > If your runtime settings implementation does not securely store values, you should set `UltraSettings.runtime_settings_secure` to `false`. This will disable runtime settings on fields marked as secret to prevent leaking sensitive information.
176
+
175
177
  #### Using the `super_settings` gem
176
178
 
177
179
  There is a companion gem [super_settings](https://github.com/bdurand/super_settings) that can be used as a drop in implementation for the runtime settings. You just need to set the runtime settings to the `SuperSettings` object.
@@ -211,7 +213,7 @@ You can customize the behavior of runtime setting names with the following optio
211
213
 
212
214
  - **Disabling Runtime Settings:** You can disable runtime settings as a default source for fields by setting `runtime_settings_disabled` to `true` in your configuration class. You can disable runtime settings on individual fields by setting `runtime_setting` on the field to `false`.
213
215
 
214
- - **Editing Links** You can specify a URL for editing runtime settings from the web UI by setting `UltraSettings.runtime_settings_url` to the desired URL. This will add links to the runtime settings in the web UI. You can use the placeholders `${name}` and `${type}` in the URL which will be replaced with the name and type of the runtime setting, respectively. If you are using the `super_settings` gem for runtime settings, then you can target a setting by adding `#edit=${name}&type=${type}` to the root URL where `super_settings` is mounted.
216
+ - **Editing Links** You can specify a URL for editing runtime settings from the web UI by setting `UltraSettings.runtime_settings_url` to the desired URL. This will add links to the runtime settings in the web UI. You can use the placeholders `${name}`, `${type}`, and `${description}` in the URL which will be replaced with the name, type, and description of the field respectively. If you are using the `super_settings` gem for runtime settings, then you can target a setting by adding `#edit=${name}&type=${type}&description=${description}` to the root URL where `super_settings` is mounted.
215
217
 
216
218
  If a setting value cannot be loaded from the runtime settings, then it's value will attempt to be loaded from a YAML file.
217
219
 
@@ -422,7 +424,7 @@ If you prefer to embed the settings view directly into your own admin tools or d
422
424
  ```erb
423
425
  <h1>Configuration</h1>
424
426
 
425
- <%= UltraSettings::ApplicationView.new.render(select_class: "form-select") %>
427
+ <%= UltraSettings::ApplicationView.new.render %>
426
428
  ```
427
429
 
428
430
  This approach allows for seamless integration of the settings UI into your application's admin interface, leveraging your existing authentication and authorization mechanisms. The settings are rendered with navigation handled by an HTML select element. You can specify the CSS classes for the select element to match your own application styles..
data/VERSION CHANGED
@@ -1 +1 @@
1
- 2.6.1
1
+ 2.8.0
@@ -0,0 +1,30 @@
1
+ <% config_class = configuration.class
2
+ has_config_file = config_class.configuration_file.is_a?(Pathname) && config_class.fields.any?(&:yaml_key) %>
3
+
4
+ <% if config_class.description || has_config_file %>
5
+ <div class="ultra-settings-description-container">
6
+ <% if config_class.description %>
7
+ <div class="ultra-settings-description">
8
+ <%= html_escape(config_class.description).gsub("\n", "<br>") %>
9
+ </div>
10
+ <% end %>
11
+
12
+ <% if has_config_file %>
13
+ <div class="ultra-settings-description">
14
+ <span class="ultra-settings-config-file-label">
15
+ Configuration file:
16
+ </span>
17
+
18
+ <code class="ultra-settings-config-file-path">
19
+ <%= html_escape(relative_path(configuration.class.configuration_file)) %>
20
+ </code>
21
+
22
+ <% unless configuration.class.configuration_file&.exist? %>
23
+ <span class="ultra-settings-file-not-found">
24
+ (file does not exist)
25
+ </span>
26
+ <% end %>
27
+ </div>
28
+ <% end %>
29
+ </div>
30
+ <% end %>
@@ -0,0 +1,14 @@
1
+ <div class="ultra-settings-configuration-list" style="display:none;">
2
+ <% configurations.each do |configuration| %>
3
+ <div class="ultra-settings-configuration-summary">
4
+ <a
5
+ href="#<%= html_escape(configuration.class.name) %>"
6
+ class="ultra-settings-configuration-title"
7
+ >
8
+ <%= html_escape(configuration.class.name) %>
9
+ </a>
10
+
11
+ <p><%= html_escape(configuration.class.description) %></p>
12
+ </div>
13
+ <% end %>
14
+ </div>
@@ -0,0 +1,53 @@
1
+ <div class="ultra-settings-dropdown" id="config-dropdown">
2
+ <button
3
+ type="button"
4
+ class="ultra-settings-dropdown-button"
5
+ id="config-dropdown-button"
6
+ >
7
+ Select Configuration
8
+ </button>
9
+
10
+ <div
11
+ class="ultra-settings-dropdown-menu"
12
+ id="config-dropdown-menu"
13
+ style="display: none;"
14
+ >
15
+ <div class="ultra-settings-search-container">
16
+ <input
17
+ type="text"
18
+ id="config-search"
19
+ placeholder="Search..."
20
+ autocomplete="off"
21
+ >
22
+ </div>
23
+
24
+ <ul class="ultra-settings-dropdown-list" id="config-list">
25
+ <% configurations.each do |config| %>
26
+ <% config_text = [
27
+ config.class.name,
28
+ config.class.description
29
+ ]
30
+ config.class.fields.each do |field|
31
+ config_text << field.name
32
+ config_text << field.description
33
+ config_text << field.env_var
34
+ config_text << field.runtime_setting
35
+ end
36
+ search_text = config_text.compact.join(" ").downcase %>
37
+
38
+ <li
39
+ class="ultra-settings-dropdown-item"
40
+ data-value="config-<%= html_escape(config.class.name) %>"
41
+ data-label="<%= html_escape(config.class.name) %>"
42
+ data-search="<%= html_escape(search_text) %>"
43
+ >
44
+ <span class="ultra-settings-check-icon"></span>
45
+
46
+ <span class="ultra-settings-config-name">
47
+ <%= html_escape(config.class.name) %>
48
+ </span>
49
+ </li>
50
+ <% end %>
51
+ </ul>
52
+ </div>
53
+ </div>
data/app/application.css CHANGED
@@ -2,22 +2,22 @@
2
2
  margin-bottom: 1rem;
3
3
  }
4
4
 
5
- .ultra-settings-nav form {
6
- margin: 0;
7
- }
8
-
9
- .ultra-settings-config-file {
5
+ .ultra-settings-description-container {
10
6
  margin-bottom: 1.5rem;
11
- padding: 1rem;
12
- background-color: var(--config-file-bg-color);
7
+ line-height: 1.5;
13
8
  border: 1px solid var(--config-file-border-color);
14
9
  border-radius: 0.375rem;
10
+ padding: 1rem 1rem 0 1rem;
11
+ }
12
+
13
+ .ultra-settings-description {
14
+ margin-bottom: 1rem;
15
+ color: var(--description-color);
15
16
  }
16
17
 
17
18
  .ultra-settings-config-file-label {
18
- font-weight: 600;
19
- color: var(--text-color);
20
- margin-right: 0.5rem;
19
+ font-weight: 500;
20
+ color: var(--description-color);
21
21
  }
22
22
 
23
23
  .ultra-settings-config-file-path {
@@ -184,9 +184,9 @@ code.ultra-settings-field-data-value {
184
184
  letter-spacing: 0.025em;
185
185
  }
186
186
 
187
- .ultra-settings-source-value-dfn {
188
- display: inline-block;
189
- margin-left: 0.25rem;
187
+ .ultra-settings-source-indicator::before {
188
+ content: "● ";
189
+ font-size: 1rem;
190
190
  }
191
191
 
192
192
  .ultra-settings-icon-info {
@@ -218,27 +218,6 @@ code.ultra-settings-field-data-value {
218
218
  opacity: 1;
219
219
  }
220
220
 
221
- .ultra-settings-select {
222
- display: inline-block;
223
- padding: .375rem 2.25rem .375rem .75rem;
224
- -moz-padding-start: calc(0.75rem - 3px);
225
- font-size: 1rem;
226
- font-weight: 400;
227
- line-height: 1.5;
228
- color: var(--form-control-color);
229
- background-color: var(--form-control-bg-color);
230
- background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23343a40' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M2 5l6 6 6-6'/%3e%3c/svg%3e");
231
- background-repeat: no-repeat;
232
- background-position: right .75rem center;
233
- background-size: 16px 12px;
234
- border: 1px solid var(--form-control-border-color);
235
- border-radius: .25rem;
236
- transition: border-color .15s ease-in-out,box-shadow .15s ease-in-out;
237
- -webkit-appearance: none;
238
- -moz-appearance: none;
239
- appearance: none;
240
- }
241
-
242
221
  .ultra-settings-dialog {
243
222
  min-width: 20rem;
244
223
  padding: 0;
@@ -273,4 +252,163 @@ code.ultra-settings-field-data-value {
273
252
  padding: 1rem;
274
253
  background-color: var(--background-color);
275
254
  color: var(--text-color);
255
+ }
256
+
257
+ /* Dropdown Styles */
258
+ .ultra-settings-dropdown {
259
+ position: relative;
260
+ display: inline-block;
261
+ width: 100%;
262
+ max-width: 400px;
263
+ }
264
+
265
+ .ultra-settings-dropdown-button {
266
+ width: 100%;
267
+ text-align: left;
268
+ padding: 0.5rem 1rem;
269
+ font-size: 1.25rem;
270
+ font-weight: 500;
271
+ background-color: var(--form-control-bg-color);
272
+ color: var(--form-control-color);
273
+ border: 1px solid var(--form-control-border-color);
274
+ border-radius: 0.375rem;
275
+ cursor: pointer;
276
+ display: flex;
277
+ justify-content: space-between;
278
+ align-items: center;
279
+ }
280
+
281
+ .ultra-settings-dropdown-button::after {
282
+ content: "";
283
+ display: inline-block;
284
+ width: 0;
285
+ height: 0;
286
+ margin-left: 0.5rem;
287
+ vertical-align: middle;
288
+ border-top: 4px solid;
289
+ border-right: 4px solid transparent;
290
+ border-left: 4px solid transparent;
291
+ }
292
+
293
+ .ultra-settings-dropdown-menu {
294
+ position: absolute;
295
+ top: 100%;
296
+ left: 0;
297
+ z-index: 1000;
298
+ display: none;
299
+ min-width: 100%;
300
+ padding: 0.5rem 0;
301
+ margin: 0.125rem 0 0;
302
+ font-size: 1rem;
303
+ color: var(--text-color);
304
+ text-align: left;
305
+ list-style: none;
306
+ background-color: var(--form-control-bg-color);
307
+ background-clip: padding-box;
308
+ border: 1px solid var(--form-control-border-color);
309
+ border-radius: 0.375rem;
310
+ box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);
311
+ }
312
+
313
+ .ultra-settings-search-container {
314
+ padding: 0.5rem 1rem;
315
+ border-bottom: 1px solid var(--form-control-border-color);
316
+ }
317
+
318
+ #config-search {
319
+ width: 100%;
320
+ padding: 0.375rem 0.75rem;
321
+ font-size: 0.875rem;
322
+ line-height: 1.5;
323
+ color: var(--form-control-color);
324
+ background-color: var(--value-bg-color);
325
+ background-clip: padding-box;
326
+ border: 1px solid var(--form-control-border-color);
327
+ border-radius: 0.25rem;
328
+ box-sizing: border-box;
329
+ }
330
+
331
+ .ultra-settings-dropdown-list {
332
+ list-style: none;
333
+ padding: 0;
334
+ margin: 0;
335
+ max-height: 300px;
336
+ overflow-y: auto;
337
+ }
338
+
339
+ .ultra-settings-dropdown-item {
340
+ display: flex;
341
+ align-items: center;
342
+ padding: 0.5rem 1rem;
343
+ clear: both;
344
+ font-weight: 400;
345
+ color: var(--form-control-color);
346
+ text-align: inherit;
347
+ white-space: nowrap;
348
+ background-color: transparent;
349
+ border: 0;
350
+ cursor: pointer;
351
+ }
352
+
353
+ .ultra-settings-dropdown-item:hover {
354
+ background-color: var(--field-header-bg-color);
355
+ }
356
+
357
+ .ultra-settings-check-icon {
358
+ width: 1.5rem;
359
+ display: inline-block;
360
+ text-align: center;
361
+ font-weight: bold;
362
+ }
363
+
364
+ .ultra-settings-dropdown-item.selected .ultra-settings-check-icon::before {
365
+ content: "✓";
366
+ }
367
+
368
+ .ultra-settings-title {
369
+ margin: 0;
370
+ font-size: 1.25rem;
371
+ font-weight: 500;
372
+ color: var(--text-color);
373
+ }
374
+
375
+ /* Configuration List Styles */
376
+ .ultra-settings-configuration-list {
377
+ display: grid;
378
+ grid-template-columns: repeat(auto-fill, minmax(max(300px, 30%), 1fr));
379
+ gap: 1.5rem;
380
+ margin-top: 1.5rem;
381
+ }
382
+
383
+ .ultra-settings-configuration-summary {
384
+ background-color: var(--field-bg-color);
385
+ border: 1px solid var(--field-border-color);
386
+ border-radius: 0.5rem;
387
+ padding: 1.5rem;
388
+ transition: transform 0.2s ease, box-shadow 0.2s ease;
389
+ overflow: hidden;
390
+ }
391
+
392
+ .ultra-settings-configuration-summary:hover {
393
+ transform: translateY(-2px);
394
+ box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.1);
395
+ }
396
+
397
+ .ultra-settings-configuration-title {
398
+ display: block;
399
+ font-size: 1.25rem;
400
+ font-weight: 600;
401
+ color: var(--description-color);
402
+ text-decoration: none;
403
+ margin-bottom: 0.75rem;
404
+ }
405
+
406
+ .ultra-settings-configuration-title:hover {
407
+ text-decoration: underline;
408
+ }
409
+
410
+ .ultra-settings-configuration-summary p {
411
+ margin: 0;
412
+ color: var(--description-color);
413
+ line-height: 1.5;
276
414
  }
data/app/application.js CHANGED
@@ -1,41 +1,124 @@
1
1
  document.addEventListener("DOMContentLoaded", () => {
2
- const menu = document.getElementById("config-selector");
2
+ const dropdown = document.getElementById("config-dropdown");
3
+ const button = document.getElementById("config-dropdown-button");
4
+ const menu = document.getElementById("config-dropdown-menu");
5
+ const searchInput = document.getElementById("config-search");
6
+ const items = document.querySelectorAll(".ultra-settings-dropdown-item");
7
+ const configurations = document.querySelectorAll(".ultra-settings-configuration");
8
+ const configList = document.querySelector(".ultra-settings-configuration-list");
3
9
 
4
- showCurrentConfiguration = () => {
5
- const selectedId = menu.options[menu.selectedIndex].value;
6
- const hash = selectedId.replace(/^config-/, "");
10
+ // If no dropdown, we might be in single config mode or no configs.
11
+ if (!dropdown) {
12
+ // If there is exactly one configuration, show it.
13
+ if (configurations.length === 1) {
14
+ configurations[0].style.display = "block";
15
+ }
16
+ return;
17
+ }
7
18
 
8
- document.querySelectorAll(".ultra-settings-configuration").forEach((configuration) => {
9
- if (configuration.id === selectedId) {
10
- configuration.style.display = "block";
11
- window.location.hash = hash;
19
+ const toggleMenu = () => {
20
+ const isVisible = menu.style.display === "block";
21
+ menu.style.display = isVisible ? "none" : "block";
22
+ if (!isVisible) {
23
+ searchInput.value = "";
24
+ filterItems("");
25
+ searchInput.focus();
26
+ }
27
+ };
28
+
29
+ const closeMenu = () => {
30
+ menu.style.display = "none";
31
+ };
32
+
33
+ const filterItems = (query) => {
34
+ const lowerQuery = query.toLowerCase();
35
+ items.forEach(item => {
36
+ const label = item.getAttribute("data-search").toLowerCase();
37
+ if (label.includes(lowerQuery)) {
38
+ item.style.display = "flex";
12
39
  } else {
13
- configuration.style.display = "none";
40
+ item.style.display = "none";
14
41
  }
15
42
  });
16
- }
43
+ };
17
44
 
18
- menu.addEventListener("change", showCurrentConfiguration);
45
+ const showConfigList = () => {
46
+ if (configList) configList.style.display = "grid";
47
+ configurations.forEach(config => config.style.display = "none");
48
+ items.forEach(item => item.classList.remove("selected"));
49
+ button.textContent = "Select Configuration";
50
+ closeMenu();
51
+ };
19
52
 
20
- const setCurrentSelection = () => {
21
- const hash = window.location.hash.replace(/^#/, "");
22
- const selectedId = `config-${hash}`;
23
- for (const option of menu.options) {
24
- if (option.value === selectedId) {
25
- option.selected = true;
26
- break;
53
+ const showConfig = (configId) => {
54
+ if (configList) configList.style.display = "none";
55
+
56
+ configurations.forEach(config => {
57
+ config.style.display = config.id === configId ? "block" : "none";
58
+ });
59
+
60
+ items.forEach(item => {
61
+ if (item.getAttribute("data-value") === configId) {
62
+ item.classList.add("selected");
63
+ button.textContent = item.getAttribute("data-label");
64
+ } else {
65
+ item.classList.remove("selected");
27
66
  }
67
+ });
68
+
69
+ closeMenu();
70
+ };
71
+
72
+ // Event Listeners
73
+ button.addEventListener("click", (e) => {
74
+ e.stopPropagation();
75
+ toggleMenu();
76
+ });
77
+
78
+ document.addEventListener("click", (e) => {
79
+ if (!dropdown.contains(e.target)) {
80
+ closeMenu();
28
81
  }
82
+ });
29
83
 
30
- showCurrentConfiguration();
31
- }
84
+ searchInput.addEventListener("input", (e) => {
85
+ filterItems(e.target.value);
86
+ });
32
87
 
33
- window.addEventListener('hashchange', setCurrentSelection);
88
+ items.forEach(item => {
89
+ item.addEventListener("click", () => {
90
+ const configId = item.getAttribute("data-value");
91
+ const hash = configId.replace(/^config-/, "");
34
92
 
93
+ if (item.classList.contains("selected")) {
94
+ // Toggle off: clear hash
95
+ history.pushState("", document.title, window.location.pathname + window.location.search);
96
+ handleHashChange();
97
+ } else {
98
+ window.location.hash = hash;
99
+ }
100
+ });
101
+ });
35
102
 
36
- if (window.location.hash) {
37
- setCurrentSelection()
38
- }
103
+ // Initial Load & Hash Change
104
+ const handleHashChange = () => {
105
+ const hash = window.location.hash.replace(/^#/, "");
106
+ if (hash) {
107
+ const configId = `config-${hash}`;
108
+ // Check if config exists
109
+ const exists = Array.from(items).some(item => item.getAttribute("data-value") === configId);
110
+ if (exists) {
111
+ showConfig(configId);
112
+ } else {
113
+ showConfigList();
114
+ }
115
+ } else {
116
+ showConfigList();
117
+ }
118
+ };
119
+
120
+ window.addEventListener("hashchange", handleHashChange);
39
121
 
40
- showCurrentConfiguration();
122
+ // Run once on load
123
+ handleHashChange();
41
124
  });