// Global state let providers = []; let authStatus = {}; let currentTab = 'credentials'; const API_BASE = window.location.origin; // DOM Elements const tabs = document.querySelectorAll('.tab'); const tabContents = document.querySelectorAll('.tab-content'); const credentialsContainer = document.getElementById('credentials-container'); const proxyContainer = document.getElementById('proxy-container'); const jsonEditor = document.getElementById('json-editor'); const importModal = document.getElementById('import-modal'); const importFile = document.getElementById('import-file'); const serviceUrlSpan = document.getElementById('service-url'); // Initialize document.addEventListener('DOMContentLoaded', () => { serviceUrlSpan.textContent = window.location.origin; loadProviders(); setupEventListeners(); }); // Tab switching tabs.forEach(tab => { tab.addEventListener('click', () => { const tabId = tab.dataset.tab; switchTab(tabId); }); }); function switchTab(tabId) { // Update active tab tabs.forEach(t => t.classList.remove('active')); document.querySelector(`[data-tab="${tabId}"]`).classList.add('active'); // Update active content tabContents.forEach(c => c.classList.remove('active')); document.getElementById(`tab-${tabId}`).classList.add('active'); currentTab = tabId; // Load proxy data if switching to proxy tab if (tabId === 'proxy') { loadProxyForms(); } // Load EPG mappings if switching to that tab if (tabId === 'epg-mapping' && window.epgMappingManager) { window.epgMappingManager.loadProviders(); } } // Load providers from API async function loadProviders() { try { const response = await fetch(`${API_BASE}/api/providers`); if (!response.ok) throw new Error(`HTTP ${response.status}`); const data = await response.json(); providers = data.providers; // Load auth status for each provider await loadAuthStatus(); renderCredentialsForms(); } catch (error) { credentialsContainer.innerHTML = `
Failed to load providers

${error.message}

Make sure the Ultimate Backend service is running.

`; console.error('Error loading providers:', error); } } // Load authentication status for all providers async function loadAuthStatus() { for (const provider of providers) { try { const response = await fetch(`${API_BASE}/api/providers/${provider.name}/auth/status`); if (response.ok) { authStatus[provider.name] = await response.json(); } } catch (error) { console.warn(`Failed to load auth status for ${provider.name}:`, error); } } } // Render credentials forms async function renderCredentialsForms() { if (providers.length === 0) { credentialsContainer.innerHTML = `

No providers configured

Check your provider configuration files

`; return; } // Load credentials for all providers in parallel const credentialsPromises = providers.map(async (provider) => { try { // Try to fetch existing credentials const credsResponse = await fetch(`${API_BASE}/api/providers/${provider.name}/credentials`); let existingCreds = null; if (credsResponse.ok) { const credsData = await credsResponse.json(); if (credsData.has_credentials) { existingCreds = credsData; } } // Check if this provider requires user credentials const isClientOnly = !provider.requires_user_credentials; const providerCardClass = isClientOnly ? 'provider-card client-credentials-only' : 'provider-card'; // Get the current auth status const status = authStatus[provider.name] || {}; // Create form groups HTML based on provider type const formGroupsHTML = isClientOnly ? '' : `
${existingCreds?.username_masked ? ` Credentials saved. Enter new values to update. ` : ''}
`; // Create buttons HTML - only show save/delete for providers that need credentials const buttonsHTML = isClientOnly ? `
` : `
${existingCreds ? ` ` : ''}
`; return `
${provider.logo ? `` : ''}

${provider.label}

${provider.name} • ${provider.country}
${getStatusIcon(status)} ${getStatusText(status)}
${isClientOnly ? `
No manual credentials required

This provider uses client credentials that are hardcoded in the application.

` : ''} ${formGroupsHTML} ${buttonsHTML}
`; } catch (error) { console.error(`Error loading credentials for ${provider.name}:`, error); return ''; // Return empty string on error } }); // Wait for all promises and render const htmls = await Promise.all(credentialsPromises); credentialsContainer.innerHTML = htmls.join(''); } // Load proxy forms async function loadProxyForms() { proxyContainer.innerHTML = await Promise.all(providers.map(async (provider) => { try { // Try to load existing proxy config const proxyResponse = await fetch(`${API_BASE}/api/providers/${provider.name}/proxy`); let existingProxy = null; if (proxyResponse.ok) { const proxyData = await proxyResponse.json(); if (proxyData.proxy_config) { existingProxy = proxyData.proxy_config; } } return `
${provider.logo ? `` : ''}

${provider.label}

Proxy Configuration
${existingProxy ? ` ` : ''}
`; } catch (error) { console.error(`Error loading proxy for ${provider.name}:`, error); return ''; // Return empty string on error } })).then(htmls => htmls.join('')); } // Helper functions function getStatusIcon(status) { if (!status) return ''; switch (status.auth_state) { case 'user_authenticated': case 'client_authenticated': return ''; case 'credentials_only': return ''; default: return ''; } } function getStatusText(status) { if (!status) return 'Unknown'; switch (status.auth_state) { case 'user_authenticated': return 'Authenticated'; case 'client_authenticated': return 'Client Authenticated'; case 'credentials_only': return 'Credentials Saved'; case 'not_authenticated': return 'Not Authenticated'; default: return status.auth_state; } } // API Functions async function saveCredentials(providerName) { // Find the provider from the global providers array const provider = providers.find(p => p.name === providerName); if (!provider) { showAlert('error', `Provider ${providerName} not found`); return; } // Check if provider is client-only if (!provider.requires_user_credentials) { showAlert('info', `${provider.label} uses hardcoded client credentials - no manual setup needed`); return; } const usernameInput = document.getElementById(`username-${providerName}`); const passwordInput = document.getElementById(`password-${providerName}`); const username = usernameInput.value; const password = passwordInput.value; // For updates, username might be readonly with masked value // We need to check if user entered a new username const isMaskedUsername = usernameInput.hasAttribute('readonly'); if (isMaskedUsername && !password) { // User is keeping existing credentials, no changes showAlert('info', 'No changes made to credentials'); return; } if (!isMaskedUsername && (!username || !password)) { showAlert('error', 'Please enter both username and password for new credentials'); return; } // Prepare credentials data const credentials = {}; // Only include username if it's not masked/readonly (new or changed) if (!isMaskedUsername && username) { credentials.username = username; } // Only include password if provided (for updates, password can be empty) if (password) { credentials.password = password; } const statusEl = document.getElementById(`status-${providerName}`); if (statusEl) { statusEl.innerHTML = ' Saving...'; } try { const response = await fetch(`${API_BASE}/api/providers/${providerName}/credentials`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(credentials) }); const result = await response.json(); if (response.ok) { if (statusEl) { statusEl.className = 'status-indicator status-success'; statusEl.innerHTML = ' Saved successfully'; } // Clear password field for security passwordInput.value = ''; // If username was changed, mark it as readonly with masked value if (!isMaskedUsername && username) { // Create a masked version for display let maskedUsername; if (username.includes('@')) { const parts = username.split('@'); maskedUsername = parts[0].substring(0, 2) + '***@' + parts[1]; } else { maskedUsername = username.substring(0, 2) + '***'; } usernameInput.value = maskedUsername; usernameInput.readOnly = true; usernameInput.style.backgroundColor = '#f5f5f5'; // Add info text const infoText = document.createElement('small'); infoText.innerHTML = ' Credentials saved. Enter new values to update.'; infoText.style.cssText = 'color:#666; display:block; margin-top:5px;'; // Remove existing info if any const existingInfo = usernameInput.nextElementSibling; if (existingInfo && existingInfo.tagName === 'SMALL') { existingInfo.remove(); } usernameInput.parentNode.insertBefore(infoText, passwordInput); } // Reload auth status await loadAuthStatus(); // Show success message const action = isMaskedUsername && !username ? 'Updated' : 'Saved'; showAlert('success', `${action} credentials for ${provider.label}`); } else { if (statusEl) { statusEl.className = 'status-indicator status-error'; statusEl.innerHTML = ` ${result.error || 'Failed to save'}`; } showAlert('error', result.error || `Failed to save credentials for ${provider.label}`); } } catch (error) { if (statusEl) { statusEl.className = 'status-indicator status-error'; statusEl.innerHTML = ' Network error'; } showAlert('error', `Network error: ${error.message}`); console.error('Save error:', error); } } async function deleteCredentials(providerName) { // Find the provider from the global providers array const provider = providers.find(p => p.name === providerName); if (!provider) { showAlert('error', `Provider ${providerName} not found`); return; } // Check if provider is client-only if (!provider.requires_user_credentials) { showAlert('info', `${provider.label} uses hardcoded client credentials - nothing to delete`); return; } if (!confirm(`Delete credentials for ${providerName}?`)) return; const statusEl = document.getElementById(`status-${providerName}`); statusEl.innerHTML = ' Deleting...'; try { const response = await fetch(`${API_BASE}/api/providers/${providerName}/credentials`, { method: 'DELETE' }); if (response.ok) { statusEl.className = 'status-indicator status-warning'; statusEl.innerHTML = ' Credentials deleted'; // Clear fields document.getElementById(`username-${providerName}`).value = ''; document.getElementById(`password-${providerName}`).value = ''; // Reload auth status await loadAuthStatus(); renderCredentialsForms(); showAlert('success', `Credentials deleted for ${providerName}`); } else { const result = await response.json(); statusEl.className = 'status-indicator status-error'; statusEl.innerHTML = ` ${result.error || 'Delete failed'}`; showAlert('error', result.error || `Failed to delete credentials for ${providerName}`); } } catch (error) { statusEl.className = 'status-indicator status-error'; statusEl.innerHTML = ' Network error'; showAlert('error', `Network error: ${error.message}`); console.error('Delete error:', error); } } async function testAuth(providerName) { const statusEl = document.getElementById(`status-${providerName}`); statusEl.innerHTML = ' Testing...'; try { const response = await fetch(`${API_BASE}/api/providers/${providerName}/auth/status`); const result = await response.json(); if (response.ok) { if (result.is_ready) { statusEl.className = 'status-indicator status-success'; statusEl.innerHTML = ' Ready to use'; showAlert('success', `${providerName} is ready to use`); } else { statusEl.className = 'status-indicator status-warning'; statusEl.innerHTML = ` ${result.auth_state}`; showAlert('warning', `${providerName} status: ${result.auth_state}`); } } else { statusEl.className = 'status-indicator status-error'; statusEl.innerHTML = ' Test failed'; showAlert('error', `Test failed for ${providerName}`); } } catch (error) { statusEl.className = 'status-indicator status-error'; statusEl.innerHTML = ' Network error'; showAlert('error', `Network error: ${error.message}`); console.error('Test error:', error); } } async function saveProxy(providerName) { const host = document.getElementById(`proxy-host-${providerName}`).value; const port = document.getElementById(`proxy-port-${providerName}`).value; const type = document.getElementById(`proxy-type-${providerName}`).value; const user = document.getElementById(`proxy-user-${providerName}`).value; const pass = document.getElementById(`proxy-pass-${providerName}`).value; if (!host || !port) { showAlert('error', 'Please fill in at least Host and Port fields'); return; } const proxyConfig = { host, port: parseInt(port), proxy_type: type }; if (user) proxyConfig.username = user; if (pass) proxyConfig.password = pass; try { const response = await fetch(`${API_BASE}/api/providers/${providerName}/proxy`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(proxyConfig) }); if (response.ok) { showAlert('success', `Proxy configuration saved for ${providerName}`); // Clear password field document.getElementById(`proxy-pass-${providerName}`).value = ''; } else { const result = await response.json(); showAlert('error', result.error || `Failed to save proxy for ${providerName}`); } } catch (error) { showAlert('error', `Network error: ${error.message}`); console.error('Proxy save error:', error); } } async function deleteProxy(providerName) { if (!confirm(`Delete proxy configuration for ${providerName}?`)) return; try { const response = await fetch(`${API_BASE}/api/providers/${providerName}/proxy`, { method: 'DELETE' }); if (response.ok) { showAlert('success', `Proxy configuration deleted for ${providerName}`); // Clear fields document.getElementById(`proxy-host-${providerName}`).value = ''; document.getElementById(`proxy-port-${providerName}`).value = ''; document.getElementById(`proxy-user-${providerName}`).value = ''; document.getElementById(`proxy-pass-${providerName}`).value = ''; } else { const result = await response.json(); showAlert('error', result.error || `Failed to delete proxy for ${providerName}`); } } catch (error) { showAlert('error', `Network error: ${error.message}`); console.error('Proxy delete error:', error); } } // UI Helper Functions function showAlert(type, message) { const alert = document.createElement('div'); alert.className = `alert alert-${type}`; alert.innerHTML = `
${message}
`; // Insert at beginning of card body const cardBody = document.querySelector('.card-body'); cardBody.insertBefore(alert, cardBody.firstChild); // Remove after 5 seconds setTimeout(() => alert.remove(), 5000); } function setupEventListeners() { // Export button document.getElementById('btn-export').addEventListener('click', exportConfig); document.getElementById('btn-export-json').addEventListener('click', exportConfig); // Import button document.getElementById('btn-import').addEventListener('click', () => importModal.style.display = 'flex'); document.getElementById('btn-import-json').addEventListener('click', () => importModal.style.display = 'flex'); // Import modal document.getElementById('btn-cancel-import').addEventListener('click', () => importModal.style.display = 'none'); document.getElementById('btn-confirm-import').addEventListener('click', importConfig); // Apply JSON config document.getElementById('btn-apply-json').addEventListener('click', applyJsonConfig); // Test connection document.getElementById('btn-test-connection').addEventListener('click', testApiConnection); // Clear all document.getElementById('btn-clear-all').addEventListener('click', clearAllConfigurations); } async function exportConfig() { try { const response = await fetch(`${API_BASE}/api/config/export`); if (!response.ok) throw new Error('Export failed'); const data = await response.json(); // Create download link const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `ultimate_backend_config_${new Date().toISOString().slice(0,10)}.json`; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); showAlert('success', 'Configuration exported successfully'); } catch (error) { showAlert('error', `Export failed: ${error.message}`); } } async function importConfig() { const file = importFile.files[0]; if (!file) { showAlert('error', 'Please select a file first'); return; } try { const text = await file.text(); const config = JSON.parse(text); const response = await fetch(`${API_BASE}/api/config/import`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(config) }); if (response.ok) { showAlert('success', 'Configuration imported successfully'); importModal.style.display = 'none'; importFile.value = ''; // Reload providers await loadProviders(); loadProxyForms(); } else { const result = await response.json(); showAlert('error', result.error || 'Import failed'); } } catch (error) { showAlert('error', `Import failed: ${error.message}`); } } async function applyJsonConfig() { const jsonText = jsonEditor.value.trim(); if (!jsonText) { showAlert('error', 'Please enter JSON configuration'); return; } try { const config = JSON.parse(jsonText); const response = await fetch(`${API_BASE}/api/config/import`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(config) }); if (response.ok) { showAlert('success', 'Configuration applied successfully'); jsonEditor.value = ''; // Reload providers await loadProviders(); loadProxyForms(); } else { const result = await response.json(); showAlert('error', result.error || 'Failed to apply configuration'); } } catch (error) { showAlert('error', `Invalid JSON: ${error.message}`); } } async function testApiConnection() { try { const response = await fetch(`${API_BASE}/api/providers`); if (response.ok) { showAlert('success', 'API connection successful'); } else { showAlert('error', `API returned ${response.status}`); } } catch (error) { showAlert('error', `API connection failed: ${error.message}`); } } async function clearAllConfigurations() { if (!confirm('Are you sure you want to clear ALL configurations? This cannot be undone.')) { return; } if (!confirm('This will delete all credentials and proxy settings. Are you REALLY sure?')) { return; } try { // Get all providers first const response = await fetch(`${API_BASE}/api/providers`); const data = await response.json(); // Delete credentials for all providers for (const provider of data.providers) { // Skip client-only providers if (provider.requires_user_credentials) { await fetch(`${API_BASE}/api/providers/${provider.name}/credentials`, { method: 'DELETE' }); } await fetch(`${API_BASE}/api/providers/${provider.name}/proxy`, { method: 'DELETE' }); } showAlert('success', 'All configurations cleared'); // Reload providers await loadProviders(); loadProxyForms(); } catch (error) { showAlert('error', `Failed to clear configurations: ${error.message}`); } } // Make functions available globally window.saveCredentials = saveCredentials; window.deleteCredentials = deleteCredentials; window.testAuth = testAuth; window.saveProxy = saveProxy; window.deleteProxy = deleteProxy;