// 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 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' && window.proxyManager) { // Filter to only enabled providers const enabledProviders = window.providerEnableManager ? window.providerEnableManager.getFilteredProviders('proxy') : providers; window.proxyManager.init(enabledProviders); window.proxyManager.loadProxyForms(); } // Load EPG mappings if switching to that tab if (tabId === 'epg-mapping' && window.epgMappingManager) { window.epgMappingManager.loadProviders(); } } // Helper function to format timestamp to local date/time function formatDateTime(timestamp) { try { if (!timestamp) return null; const date = new Date(timestamp * 1000); // Convert Unix timestamp to milliseconds if (isNaN(date.getTime())) return null; const day = String(date.getDate()).padStart(2, '0'); const month = String(date.getMonth() + 1).padStart(2, '0'); const year = date.getFullYear(); const hours = String(date.getHours()).padStart(2, '0'); const minutes = String(date.getMinutes()).padStart(2, '0'); return `${day}.${month}.${year} ${hours}:${minutes}`; } catch (error) { console.error('Error formatting date:', error); return null; } } // Helper function to format relative time function formatRelativeTime(seconds) { try { if (seconds === null || seconds === undefined) return null; const absSeconds = Math.abs(seconds); const isPast = seconds < 0; if (absSeconds < 60) { return isPast ? 'just expired' : 'in less than a minute'; } const minutes = Math.floor(absSeconds / 60); if (minutes < 60) { const text = minutes === 1 ? '1 minute' : `${minutes} minutes`; return isPast ? `${text} ago` : `in ${text}`; } const hours = Math.floor(minutes / 60); if (hours < 24) { const text = hours === 1 ? '1 hour' : `${hours} hours`; return isPast ? `${text} ago` : `in ${text}`; } const days = Math.floor(hours / 24); const text = days === 1 ? '1 day' : `${days} days`; return isPast ? `${text} ago` : `in ${text}`; } catch (error) { console.error('Error formatting relative time:', error); return null; } } // Helper function to get authentication type description function getAuthTypeDescription(status) { try { if (!status || !status.auth_type) { return 'Unknown authentication'; } const authType = status.auth_type; // Map auth types to readable descriptions const authTypeMap = { 'user_credentials': 'User Credentials authentication', 'client_credentials': 'Client Credentials authentication', 'anonymous': 'Anonymous authentication', 'network_based': 'Network-based authentication', 'device_registration': 'Device Registration authentication', 'embedded_client': 'Embedded Client authentication' }; return authTypeMap[authType] || `${authType.replace(/_/g, ' ')} authentication`; } catch (error) { console.error('Error getting auth type description:', error); return 'Unknown authentication'; } } // Helper function to format token expiration info function formatTokenExpiration(status) { try { if (!status) return ''; let expirationHTML = ''; // Main token expiration if (status.token_expires_at) { const formattedDate = formatDateTime(status.token_expires_at); const relativeTime = formatRelativeTime(status.token_expires_in_seconds); if (formattedDate && relativeTime) { if (status.token_expires_in_seconds < 0) { // Expired expirationHTML += `
Token expired ${relativeTime}
`; } else { // Valid expirationHTML += `
Token expires: ${formattedDate} (${relativeTime})
`; } } } // Refresh token expiration (if available) if (status.refresh_token_expires_at) { const formattedDate = formatDateTime(status.refresh_token_expires_at); const relativeTime = formatRelativeTime(status.refresh_token_expires_in_seconds); if (formattedDate && relativeTime) { if (status.refresh_token_expires_in_seconds < 0) { // Expired expirationHTML += `
Refresh token expired ${relativeTime}
`; } else { // Valid expirationHTML += `
Refresh token expires: ${formattedDate} (${relativeTime})
`; } } } return expirationHTML; } catch (error) { console.error('Error formatting token expiration:', error); return ''; } } // 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; // Enabled providers with full details // NEW: Store all_providers metadata for enable/disable functionality window.allProvidersMetadata = data.all_providers || []; // Initialize enable manager with all_providers metadata if (window.providerEnableManager) { await window.providerEnableManager.init(data.all_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() { // Use all_providers metadata instead of just enabled providers const allProviders = window.allProvidersMetadata || []; if (allProviders.length === 0) { credentialsContainer.innerHTML = `

No providers configured

Check your provider configuration files

`; return; } // Add filter UI if (window.addFilterUI) { window.addFilterUI(); } // Load credentials for all providers in parallel const credentialsPromises = allProviders.map(async (providerMetadata) => { const provider_name = providerMetadata.name; // Get full provider details from the enabled providers array if available const provider = providers.find(p => p.name === provider_name) || { name: provider_name, label: providerMetadata.label, logo: providerMetadata.logo, country: providerMetadata.country, auth: providerMetadata.auth || {} }; try { // Only try to fetch credentials for enabled providers with full details let existingCreds = null; // Only fetch credentials if provider is enabled and instance_ready if (providerMetadata.enabled && providerMetadata.instance_ready) { try { const credsResponse = await fetch(`${API_BASE}/api/providers/${provider_name}/credentials`); if (credsResponse.ok) { const credsData = await credsResponse.json(); if (credsData.has_credentials) { existingCreds = credsData; } } } catch (credErr) { console.debug(`Could not fetch credentials for ${provider_name}:`, credErr); } } // Get auth information from provider const auth = provider.auth || {}; // Determine UI based on auth properties const needsUserCredentials = auth.needs_user_credentials || false; const needsClientCredentials = auth.needs_client_credentials || false; const isAnonymous = auth.is_anonymous || false; const isNetworkBased = auth.is_network_based || false; const usesEmbeddedClient = auth.uses_embedded_client || false; const usesDeviceRegistration = auth.uses_device_registration || false; // Get current auth status (only for enabled providers) const status = (providerMetadata.enabled && providerMetadata.instance_ready) ? (authStatus[provider_name] || {}) : {}; // Get enable/disable status from metadata const isEnabled = providerMetadata.enabled && providerMetadata.instance_ready; const canModify = window.providerEnableManager ? window.providerEnableManager.canModify(provider_name) : true; // Determine provider card class let providerCardClass = 'provider-card'; if (!needsUserCredentials) { providerCardClass += ' client-credentials-only'; } if (!isEnabled) { providerCardClass += ' disabled'; } // Create toggle switch const toggleSwitch = window.createToggleSwitch ? window.createToggleSwitch(provider_name, isEnabled, canModify) : ''; // Create form content based on auth type let formContent = ''; if (needsUserCredentials) { // User credentials form formContent = `
${existingCreds?.username_masked ? ` Credentials saved. Enter new values to update. ` : ''}
`; } else if (needsClientCredentials && !needsUserCredentials) { // Client credentials only (no user input needed) formContent = `
Client Credentials Only

This provider uses hardcoded client credentials that don't require manual setup.

`; } else if (isAnonymous || isNetworkBased || usesEmbeddedClient) { // No credentials required formContent = `
No Credentials Required

This provider uses ${auth.preferred_auth_type?.replace('_', ' ') || 'automatic'} authentication.

`; } else if (usesDeviceRegistration) { // Device registration formContent = `
Device Registration

This provider requires device registration. Follow provider-specific setup instructions.

`; } else { // Fallback for unknown auth types formContent = `
Authentication Type Unknown

This provider's authentication method could not be determined.

`; } // Create buttons based on auth type and enabled state let buttonsHTML = ''; if (isEnabled && needsUserCredentials) { buttonsHTML = `
${existingCreds ? ` ` : ''}
`; } else if (isEnabled) { // For non-user-credential providers, only show test button buttonsHTML = `
`; } return `
${provider.logo ? `` : ''}

${provider.label}

${provider_name} • ${provider.country}
${isEnabled ? `
${getAuthTypeDescription(status)}
${getStatusIcon(status)} ${getStatusText(status)}
${formatTokenExpiration(status)} ` : `
Provider Disabled
`}
${toggleSwitch}
${formContent} ${buttonsHTML}
`; } catch (error) { console.error(`Error loading credentials for ${provider_name}:`, error); return ''; // Return empty string on error } }); // Wait for all promises and render try { const htmls = await Promise.all(credentialsPromises); credentialsContainer.innerHTML = htmls.join(''); // Apply current filter if (window.providerEnableManager) { window.providerEnableManager.applyFilter(); } } catch (error) { console.error('Error rendering credentials forms:', error); credentialsContainer.innerHTML = `
Failed to render credentials

${error.message}

`; } } // Helper functions function getStatusIcon(status) { if (!status) return ''; switch (status.auth_state) { case 'authenticated': case 'AUTHENTICATED': case 'user_authenticated': case 'client_authenticated': return ''; case 'expired': case 'EXPIRED': case 'credentials_only': return ''; case 'not_authenticated': case 'NOT_AUTHENTICATED': return ''; case 'not_applicable': case 'NOT_APPLICABLE': return ''; default: return ''; } } function getStatusText(status) { if (!status) return 'Unknown'; // Normalize auth_state to lowercase for comparison const authState = (status.auth_state || '').toLowerCase(); switch (authState) { case 'authenticated': return 'Authenticated'; case 'expired': return 'Token Expired (can refresh)'; case 'not_authenticated': return 'Not Authenticated'; case 'not_applicable': return 'No Auth Required'; case 'user_authenticated': return 'User Authenticated'; case 'client_authenticated': return 'Client Authenticated'; case 'credentials_only': return 'Credentials Saved'; default: return status.auth_state || 'Unknown'; } } // 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 auth properties const auth = provider.auth || {}; // Check if provider actually needs user credentials if (!auth.needs_user_credentials) { const authType = auth.preferred_auth_type || 'unknown'; const authTypeName = authType.replace('_', ' '); if (auth.needs_client_credentials && !auth.needs_user_credentials) { showAlert('info', `${provider.label} uses client credentials - credentials are hardcoded in the application`); } else if (auth.is_anonymous) { showAlert('info', `${provider.label} is an anonymous provider - no credentials needed`); } else if (auth.is_network_based) { showAlert('info', `${provider.label} uses network-based authentication - no manual setup needed`); } else if (auth.uses_embedded_client) { showAlert('info', `${provider.label} uses embedded client credentials - no manual setup needed`); } else if (auth.uses_device_registration) { showAlert('info', `${provider.label} requires device registration - follow provider setup instructions`); } else { showAlert('info', `${provider.label} uses ${authTypeName} authentication - no manual credential setup`); } 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.parentNode); } // Reload auth status await loadAuthStatus(); // Re-render to show updated status and expiration await renderCredentialsForms(); // 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 auth properties const auth = provider.auth || {}; // Check if provider actually uses user credentials if (!auth.needs_user_credentials) { const authType = auth.preferred_auth_type || 'unknown'; const authTypeName = authType.replace('_', ' '); if (auth.needs_client_credentials && !auth.needs_user_credentials) { showAlert('info', `${provider.label} uses hardcoded client credentials - nothing to delete`); } else if (auth.is_anonymous) { showAlert('info', `${provider.label} is an anonymous provider - no credentials to delete`); } else if (auth.is_network_based) { showAlert('info', `${provider.label} uses network-based authentication - no credentials stored`); } else if (auth.uses_embedded_client) { showAlert('info', `${provider.label} uses embedded client credentials - nothing to delete`); } else if (auth.uses_device_registration) { showAlert('info', `${provider.label} uses device registration - no stored credentials to delete`); } else { showAlert('info', `${provider.label} uses ${authTypeName} authentication - no credentials to delete`); } return; } if (!confirm(`Delete credentials for ${providerName}?`)) return; const statusEl = document.getElementById(`status-${providerName}`); if (statusEl) { statusEl.innerHTML = ' Deleting...'; } try { const response = await fetch(`${API_BASE}/api/providers/${providerName}/credentials`, { method: 'DELETE' }); if (response.ok) { // Reload auth status await loadAuthStatus(); renderCredentialsForms(); showAlert('success', `Credentials deleted for ${providerName}`); } else { const result = await response.json(); if (statusEl) { statusEl.className = 'status-indicator status-error'; statusEl.innerHTML = ` ${result.error || 'Delete failed'}`; } showAlert('error', result.error || `Failed to delete credentials for ${providerName}`); } } catch (error) { if (statusEl) { 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}`); if (!statusEl) { console.error(`Status element not found for ${providerName}`); return; } statusEl.innerHTML = ' Testing...'; try { const response = await fetch(`${API_BASE}/api/providers/${providerName}/auth/status`); const result = await response.json(); if (response.ok) { // Update auth status in memory authStatus[providerName] = result; // Re-render to show updated status and expiration await renderCredentialsForms(); if (result.is_ready) { showAlert('success', `${providerName} is ready to use`); } else { 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); } } // 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(); } 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(); } 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' }); } // Delete proxy configurations await fetch(`${API_BASE}/api/providers/${provider.name}/proxy`, { method: 'DELETE' }); } showAlert('success', 'All configurations cleared'); // Reload providers await loadProviders(); // Reload proxy forms if proxy manager exists and we're on proxy tab if (window.proxyManager && currentTab === 'proxy') { window.proxyManager.init(data.providers); window.proxyManager.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.loadProviders = loadProviders; // Export for provider toggle reload