This document describes the refactoring of the settings page to eliminate code duplication and improve maintainability by using reusable Alpine.js components, plus the transition from a tabbed interface to a unified single-page view.
The original settings.html file had 986 lines with significant duplication:
Refactored the page to use data-driven rendering with Alpine.js:
services: [
{
id: 'radarr', // Unique service identifier
label: 'Radarr', // Display name
fields: [ // Form fields configuration
{
name: 'url', // Field name (maps to config.services.radarr.url)
label: 'URL', // Display label
type: 'url', // Input type
placeholder: 'http://...' // Placeholder text
},
// ... more fields
],
systemInfoFields: [ // System info display configuration
{
key: 'version', // Key in system_info response
label: 'Version', // Display label
mono: false, // Use monospace font?
span: false // Span 2 columns?
},
// ... more fields
]
},
// ... more services
]
settings() // Main Alpine.js component
├── services[] // Service metadata array
├── config.services.* // Service configurations
├── connectionResults.* // Connection test results
└── envSources.* // Environment variable sources
systemInfoDisplay() // System info display component
├── systemInfo // System info data
├── fieldsDef // Field definitions
└── displayFields // Computed display fields
<template x-for="service in services" :key="service.id">
<div class="bg-dark-surface ...">
<!-- Panel content always visible -->
</div>
</template>
<template x-for="field in service.fields" :key="field.name">
<input
:type="field.type"
x-model="config.services[service.id][field.name]"
:placeholder="field.placeholder">
</template>
<template x-for="field in getSystemInfoDisplay(service.id)" :key="field.key">
<p x-show="field.html" x-html="field.value"></p>
<p x-show="!field.html" x-text="field.value"></p>
</template>
async testAllConnections() {
this.testing = true;
this.connectionResults = {};
// Test all enabled services sequentially
for (const service of this.services) {
if (this.config.services[service.id]?.enabled) {
await this.testConnection(service.id);
}
}
this.testing = false;
}
XSS Prevention: All user-controlled content is properly escaped
escapeHtml()x-text used by default (safe), x-html only for controlled boolean badgesneedsHtml() flag explicitly controls HTML renderingBefore (vulnerable):
formatValue(key, value) {
return String(value); // No escaping - XSS risk!
}
After (secure):
formatValue(key, value) {
if (typeof value === 'boolean') {
return '<span>...</span>'; // Controlled HTML for badges
}
return this.escapeHtml(String(value)); // All other values escaped
}
Avoided Re-initialization: System info display computed once in parent component
x-data that re-initialized on visibility changesgetSystemInfoDisplay() method called from parent scopeUnified Single-Page View: All configurations visible without navigation
Before: Adding a new service
<!-- ~140 lines of HTML -->
<div x-show="activeTab === 'newservice'">
<div class="flex items-center...">
<h2>New Service Configuration</h2>
<!-- ... -->
</div>
<div class="space-y-4">
<div>
<label>URL</label>
<input type="url" x-model="config.services.newservice.url" ...>
</div>
<!-- ... more fields ... -->
<button @click="testConnection('newservice')">Test</button>
<!-- ... connection results ... -->
</div>
</div>
After: Adding a new service
// ~15 lines of metadata
{
id: 'newservice',
label: 'New Service',
fields: [
{ name: 'url', label: 'URL', type: 'url', placeholder: 'http://...' },
{ name: 'api_key', label: 'API Key', type: 'password', placeholder: '...' }
],
systemInfoFields: [
{ key: 'version', label: 'Version' }
]
}
All services automatically have:
Individual Tests: Test specific services independently
<button @click="testConnection(service.id)">Test Connection</button>
Bulk Testing: Test all enabled services at once
<button @click="testAllConnections()">Test All Connections</button>
The testAllConnections() method:
Fixed once → applies to all services
Easy to add features globally:
The refactored version maintains compatibility with environment variable overrides:
<input
x-model="config.services[service.id][field.name]"
:disabled="envSources[service.id]?.[field.name]"
:class="envSources[service.id]?.[field.name] ? 'bg-slate-800/50 cursor-not-allowed' : 'bg-dark-bg'">
The systemInfoDisplay() helper provides secure formatting for different value types with XSS prevention:
formatValue(key, value) {
if (value === null || value === undefined) return 'N/A';
if (typeof value === 'boolean') {
// Return HTML for boolean values (styled badges)
return value ?
'<span class="px-2 py-1 bg-green-900/30 border border-green-600/50 text-green-300 rounded text-xs">Yes</span>' :
'<span class="px-2 py-1 bg-gray-900/30 border border-gray-600/50 text-gray-300 rounded text-xs">No</span>';
}
// Return escaped plain text for all other values
return this.escapeHtml(String(value));
}
escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
The component uses a dual rendering approach for security:
x-text for plain text values (most cases) - safe from XSSx-html only for boolean badges - controlled HTML outputneedsHtml() method determines which renderer to useSystem info display is computed in the parent component rather than using nested x-data:
// In settings component
getSystemInfoDisplay(serviceId) {
const service = this.services.find(s => s.id === serviceId);
const systemInfo = this.connectionResults[serviceId]?.system_info;
if (!systemInfo || !service) {
return [];
}
// Use the systemInfoDisplay helper to generate fields
const helper = systemInfoDisplay(systemInfo, service.systemInfoFields || []);
return helper.displayFields;
}
This avoids re-initialization of the Alpine component on every visibility change.
Connection testing works the same way for all services:
async testConnection(service) {
this.testing = true;
this.connectionResults[service] = null;
const response = await fetch(`/api/config/test/${service}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(this.config.services[service])
});
// ... handle response ...
}
Since services need to be started by the user, manual testing should verify:
Possible improvements for future iterations:
For developers working with this code:
services array:
{
id: 'bazarr',
label: 'Bazarr',
fields: [
{ name: 'url', label: 'URL', type: 'url', placeholder: 'http://localhost:6767' },
{ name: 'api_key', label: 'API Key', type: 'password', placeholder: 'Enter Bazarr API Key' }
],
systemInfoFields: [
{ key: 'version', label: 'Version' },
{ key: 'branch', label: 'Branch' }
]
}
internal/config/config.go:
```go
type ClientsConfig struct {
// … existing clients …
Bazarr BazarrConfig mapstructure:"bazarr"
}type BazarrConfig struct {
Enabled bool mapstructure:"enabled"
URL string mapstructure:"url"
APIKey string mapstructure:"api_key"
}
3. Implement connection test in `internal/handler/settings.go`
### Modifying Field Display
To change how a field is rendered, modify the field template:
```html
<template x-for="field in service.fields" :key="field.name">
<div>
<!-- Add custom rendering logic here -->
<input
:type="field.type || 'text'"
x-model="config.services[service.id][field.name]"
:placeholder="field.placeholder">
</div>
</template>
To customize system info display for a specific service, define systemInfoFields:
systemInfoFields: [
{ key: 'version', label: 'Version' },
{ key: 'server_id', label: 'Server ID', mono: true }, // Monospace font
{ key: 'local_address', label: 'Address', mono: true, span: true } // Full width
]
This refactoring significantly improves the maintainability and extensibility of the settings page while reducing code size by 54%. The data-driven approach makes it easy to add new services and modify behavior globally.
web/templates/pages/settings-original-backup.htmlweb/templates/pages/settings.html