keepercheky

Settings Page Refactoring

Overview

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.

Problem

The original settings.html file had 986 lines with significant duplication:

Solution

Refactored the page to use data-driven rendering with Alpine.js:

Architecture

Service Metadata Structure

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
]

Component Hierarchy

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

Dynamic Rendering Flow

  1. Panel Generation (All visible simultaneously)
    <template x-for="service in services" :key="service.id">
        <div class="bg-dark-surface ...">
            <!-- Panel content always visible -->
        </div>
    </template>
    
  2. Field Generation
    <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>
    
  3. System Info Display
    <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>
    
  4. Test All Connections
    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;
    }
    

Key Improvements

1. Security

XSS Prevention: All user-controlled content is properly escaped

Before (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
}

2. Performance

Avoided Re-initialization: System info display computed once in parent component

3. User Experience

Unified Single-Page View: All configurations visible without navigation

4. Maintainability

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' }
    ]
}

5. Consistency

All services automatically have:

6. Testing Workflow

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:

7. Bug Fixes

Fixed once → applies to all services

4. Extensibility

Easy to add features globally:

Implementation Details

Environment Variable Handling

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'">

Dynamic System Info

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:

Performance Optimization

System 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

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 ...
}

Testing

Automated Tests

Manual Testing Required

Since services need to be started by the user, manual testing should verify:

Future Enhancements

Possible improvements for future iterations:

  1. Field Validation
    • Add validation rules to field metadata
    • Display validation errors inline
  2. Conditional Fields
    • Show/hide fields based on other field values
    • Example: Advanced settings toggle
  3. Field Groups
    • Group related fields together
    • Collapsible sections
  4. Custom Field Types
    • File picker
    • Color picker
    • Multi-select
  5. Service Templates
    • Define service types (arr-based, media-server, etc.)
    • Inherit common configuration

Migration Guide

For developers working with this code:

Adding a New Service

  1. Add service metadata to 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' }
     ]
    }
    
  2. Add backend configuration in 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>

Customizing System Info

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
]

Conclusion

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.

References