### Clone and Start Rotato Project Source: https://github.com/p32929/rotato/blob/master/README.md Clone the repository, navigate to the directory, copy the example environment file, edit it with your settings, and start the application. ```bash git clone https://github.com/p32929/openai-gemini-api-key-rotator.git cd openai-gemini-api-key-rotator cp .env.example .env # Edit .env: Set PORT and ADMIN_PASSWORD npm start ``` -------------------------------- ### Render Environment Variables Source: https://github.com/p32929/rotato/blob/master/public/admin.html Initiates the rendering of environment variables, starting with rendering providers. ```javascript let originalValues = {}; let keyUsageData = {}; function renderEnvVars() { // Render providers renderProviders(); } ``` -------------------------------- ### File Logging Format Example Source: https://github.com/p32929/rotato/blob/master/README.md This is an example of a single line logged to `logs.jsonl`. It shows the structure of the JSON object containing details of an API request. ```json {"timestamp":"2026-04-01T15:33:15.221Z","requestId":"h8j4pqqot","method":"POST","endpoint":"/chat/completions","provider":"cerebras","status":200,"responseTime":497,"error":null,"clientIp":"::ffff:127.0.0.1","keyUsed":"csk-...9ft2","failedKeys":[]} ``` -------------------------------- ### OpenAI-Compatible API Request Source: https://github.com/p32929/rotato/blob/master/README.md Example of how to make a POST request to the OpenAI-compatible chat completions endpoint. ```APIDOC ## POST /groq/chat/completions ### Description Sends a request to the chat completions endpoint compatible with OpenAI's API. ### Method POST ### Endpoint `http://localhost:8990/groq/chat/completions` ### Headers - `Authorization`: Bearer [ACCESS_KEY:your-access-key] - `Content-Type`: application/json ### Request Body - `model` (string) - Required - The model to use for generation. - `messages` (array) - Required - An array of message objects representing the conversation. - `role` (string) - Required - The role of the message sender (e.g., "user", "system", "assistant"). - `content` (string) - Required - The content of the message. ### Request Example ```json { "model": "openai/gpt-oss-120b", "messages": [ { "role": "user", "content": "Hello! Please say hello back." } ] } ``` ### Response #### Success Response (200) Details of the success response are not provided in the source. ``` -------------------------------- ### Gemini-Compatible API Request Source: https://github.com/p32929/rotato/blob/master/README.md Example of how to make a POST request to the Gemini-compatible content generation endpoint. ```APIDOC ## POST /gemini/models/{model}:generateContent ### Description Sends a request to the content generation endpoint compatible with Gemini's API. ### Method POST ### Endpoint `http://localhost:8990/gemini/models/gemini-2.5-flash:generateContent` ### Headers - `x-goog-api-key`: [ACCESS_KEY:your-access-key] - `Content-Type`: application/json ### Request Body - `contents` (array) - Required - An array of content objects. - `parts` (array) - Required - An array of parts within the content. - `text` (string) - Required - The text content of the part. ### Request Example ```json { "contents": [ { "parts": [ { "text": "Hello! Please say hello back." } ] } ] } ``` ### Response #### Success Response (200) Details of the success response are not provided in the source. ``` -------------------------------- ### Get Default Provider Configuration Source: https://github.com/p32929/rotato/blob/master/public/admin.html Retrieves the default configuration for a given API type, including its name and base URL. ```javascript function getDefaultProviderConfig(apiType) { const defaults = { openai: { name: 'openai', baseUrl: 'https://api.openai.com/v1' }, gemini: { name: 'gemini', baseUrl: 'https://generativelanguage.googleapis.com/v1' } }; return defaults[apiType] || null; } ``` -------------------------------- ### Start Login Countdown Timer Source: https://github.com/p32929/rotato/blob/master/public/admin.html Initiates a countdown timer for the login attempt, disabling input fields and the login button. It's used when the user is rate-limited, providing visual feedback on the remaining time. ```javascript function startLoginCountdown(seconds, passwordInput, loginButton, errorDiv) { ``` -------------------------------- ### Setup Notification Hover Interaction Source: https://github.com/p32929/rotato/blob/master/public/admin.html Manages the display and hiding of expanded notifications when hovering over notification elements in the header. Includes timeouts to prevent flickering. ```javascript function setupNotificationHover() { const headerNotifications = document.getElementById('headerNotifications'); const singleNotification = document.getElementById('singleNotification'); const expandedNotifications = document.getElementById('expandedNotifications'); if (!headerNotifications || !singleNotification || !expandedNotifications) return; let hoverTimeout; // Show expanded view on hover const showExpanded = () => { clearTimeout(hoverTimeout); // Only show expanded view if there are notifications if (activeNotifications.length > 0) { updateExpandedNotifications(); positionNotifications(); expandedNotifications.classList.remove('hidden'); } }; headerNotifications.addEventListener('mouseenter', showExpanded); singleNotification.addEventListener('mouseenter', showExpanded); // Hide expanded view on leave with small delay headerNotifications.addEventListener('mouseleave', () => { hoverTimeout = setTimeout(() => { expandedNotifications.classList.add('hidden'); }, 200); // Small delay to prevent flickering }); // Keep expanded view open if hovering over it expandedNotifications.addEventListener('mouseenter', () => { clearTimeout(hoverTimeout); }); expandedNotifications.addEventListener('mouseleave', () => { hoverTimeout = setTimeout(() => { expandedNotifications.classList.add('hidden'); }, 200); }); } ``` -------------------------------- ### Initialize Admin Panel UI and Functionality Source: https://github.com/p32929/rotato/blob/master/public/admin.html Sets up the admin panel on page load, including theme initialization, active tab styling, provider preview, sticky header adjustment, notification hover, and authentication check. ```javascript document.addEventListener('DOMContentLoaded', function() { // Initialize theme initializeTheme(); const activeTab = document.querySelector('.tab-btn.active'); if (activeTab) { activeTab.classList.add('text-foreground', 'border-primary'); activeTab.classList.remove('text-muted-foreground'); } // Initialize provider preview updateProviderPreview(); // Fix sticky header positioning adjustStickyHeaderPositioning(); // Re-adjust on window resize window.addEventListener('resize', adjustStickyHeaderPositioning); // Setup notification hover functionality setupNotificationHover(); // Check if user is already authenticated checkAuth(); }); ``` -------------------------------- ### Export Configuration as Environment Variables Source: https://github.com/p32929/rotato/blob/master/public/admin.html Fetches the .env file content from the server, replaces the ADMIN_PASSWORD value with an empty string, and copies the modified content to the clipboard. Handles potential fetch errors. ```javascript async function exportConfiguration() { try { // Fetch the .env file content from the server const response = await fetch('/admin/api/env-file'); if (!response.ok) { throw new Error('Failed to fetch .env file'); } let envContent = await response.text(); // Replace ADMIN_PASSWORD value with empty string envContent = envContent.replace(/^ADMIN_PASSWORD=.*$/m, 'ADMIN_PASSWORD='); // Copy to clipboard await copyToClipboard(envContent, '.env '); } catch (error) { showErrorToast(`Error exporting configuration: ${error.message}`); } } ``` -------------------------------- ### Initialize Theme from Local Storage Source: https://github.com/p32929/rotato/blob/master/public/admin.html Sets the initial theme (light or dark) based on the value stored in localStorage or the user's system preference if no preference is saved. It applies the 'dark' class accordingly. ```javascript function initializeTheme() { const savedTheme = localStorage.getItem('theme'); const body = document.getElementById('mainBody'); const themeToggle = document.getElementById('themeToggle'); // Default to dark mode if no preference is saved if (savedTheme === 'dark' || (!savedTheme && window.matchMedia('(prefers-color-scheme: dark)').matches)) { body.classList.add('dark'); if (themeToggle) { themeToggle.classList.add('dark'); } } } ``` -------------------------------- ### Telegram Bot Environment Configuration Source: https://github.com/p32929/rotato/blob/master/README.md Set up the Telegram bot by providing your bot token from BotFather and optionally specifying allowed user IDs. Leave TELEGRAM_ALLOWED_USERS empty to allow any user. ```env TELEGRAM_BOT_TOKEN=your-bot-token-from-botfather TELEGRAM_ALLOWED_USERS=123456789,987654321 ``` -------------------------------- ### Add New API Key Input and Actions Source: https://github.com/p32929/rotato/blob/master/public/admin.html Provides the UI elements for entering a new API key and buttons to test and save it. Includes a placeholder for displaying the result of the new key test. ```html
``` -------------------------------- ### Rotato Environment Configuration Source: https://github.com/p32929/rotato/blob/master/README.md Configure the application's port and the admin panel password by editing the .env file. Access the admin panel at http://localhost:8990/admin. ```env PORT=8990 ADMIN_PASSWORD=your-secure-password ``` -------------------------------- ### Load Environment Variables Source: https://github.com/p32929/rotato/blob/master/public/admin.html Asynchronously loads environment variables, key usage data, and settings from multiple API endpoints. Handles potential errors during loading. ```javascript async function loadEnvVars() { try { const [envResponse, usageResponse, settingsResponse] = await Promise.all([ fetch('/admin/api/env'), fetch('/admin/api/key-usage').catch(() => ({ ok: false })), fetch('/admin/api/telegram').catch(() => ({ ok: false })) ]); envVars = await envResponse.json(); if (usageResponse.ok) { keyUsageData = await usageResponse.json(); } if (settingsResponse.ok) { const settings = await settingsResponse.json(); defaultStatusCodes = settings.defaultStatusCodes || '429'; } renderEnvVars(); updateProviderPreview(); } catch (error) { showError('envError', 'Failed to load environment variables: ' + error.message); } } ``` -------------------------------- ### Add and Save Provider API Key Source: https://github.com/p32929/rotato/blob/master/public/admin.html Tests a new API key for a given provider, and if valid, adds it to the environment variables and saves them. Reverts changes if saving fails. ```javascript async function testProviderNewKey(apiType, providerName) { const inputId = `newKey_${apiType}_${providerName}`; const newKey = document.getElementById(inputId).value.trim(); if (!newKey) { showWarningToast('Please enter an API key to test'); return; } // Get provider's base URL const keysVar = `${apiType.toUpperCase()}_${providerName.toUpperCase()}_API_KEYS`; const baseUrlVar = `${apiType.toUpperCase()}_${providerName.toUpperCase()}_BASE_URL`; const baseUrl = envVars[baseUrlVar] || null; const maskedKey = newKey.length > 16 ? newKey.substring(0, 8) + '...' + newKey.slice(-4) : newKey; showInfoToast(`Testing API key ${maskedKey} for provider '${providerName}'...`); try { const response = await fetch('/admin/api/test', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ apiType: apiType, apiKey: newKey, baseUrl: baseUrl }) }); const result = await response.json(); if (result.success) { showSuccessToast(`✅ API key ${maskedKey} is valid! Ready to add to provider '${providerName}'.`); } else { showErrorToast(`❌ API key ${maskedKey} is invalid: ${result.error || 'Unknown error'}`); } } catch (error) { showErrorToast(`❌ API key test failed: ${error.message}`); } } ``` -------------------------------- ### Render Providers for Environment Variables Source: https://github.com/p32929/rotato/blob/master/public/admin.html Renders the list of providers based on loaded environment variables. It groups variables by provider type and name. ```javascript function renderProviders() { const providersContainer = document.getElementById('providersContainer'); providersContainer.innerHTML = ''; // Group environment variables by provider const providers = {}; for (const [key, value] of Object.entries(envVars)) { if (key.endsWith('_API_KEYS') && value) { // First try to match the pattern APITYPE_PROVIDERNAME_API_KEYS // We need to split only on the FIRST underscore to preserve provider names with underscores const keyWithoutSuffix = key.replace('_API_KEYS', ''); const firstUnderscoreIndex = keyWithoutSuffix.indexOf('_'); if (firstUnderscoreIndex > 0) { const apiType = keyWithoutSuffix.substring(0, firstUnderscoreIndex).toLowerCase(); const providerName = keyWithoutSuffix.substring(firstUnderscoreIndex + 1).toLowerCase(); const providerKey = `${apiType}_${providerName}`; if (!providers[providerKey]) { providers[providerKey] = { name: providerName, apiType: apiType, keys: [], baseUrl: '', accessKey: '', defaultModel: '', modelHistory: [] }; } const rawKeys = value.split(',').map(k => k.trim()).filter(k => k); providers[providerKey].keys = rawKeys.map(k => { if (k.startsWith('~')) { return { key: k.substring(1), disabled: true }; } return { key: k, disabled: false }; }); } } else if (key.endsWith('_DISABLED') && value) { const keyWithoutSuffix = key.replace('_DISABLED', ''); const firstUnderscoreIndex = keyWithoutSuffix.indexOf('_'); if (firstUnderscoreIndex > 0) { const apiType = keyWithoutSuffix.substring(0, firstUnderscoreIndex).toLowerCase(); const providerName = keyWithoutSuffix.substring(firstUnderscoreIndex + 1).toLowerCase(); const providerKey = `${apiType}_${providerName}`; if (!providers[providerKey]) { providers[providerKey] = { name: providerName, apiType: apiType, keys: [], baseUrl: '', accessKey: '', defaultModel: '', modelHistory: [] }; } providers[providerKey].disabled = (value.trim().toLowerCase() === 'true'); } } else if (key.endsWith('_BASE_URL') && value) { const match = key.match(/^(.+)_([^_]+)_BASE_URL$/); if (match && match.length >= 3) { const apiType = match[1].toLowerCase(); const providerName = match[2].toLowerCase(); const providerKey = `${apiType}_${providerName}`; if (!providers[providerKey]) { providers[providerKey] = { name: providerName, apiType: apiType, keys: [], baseUrl: '', accessKey: '', defaultModel: '', modelHistory: [] }; } providers[providerKey].baseUrl = value; } } else if (key.endsWith('_ACCESS_KEY') && value) { const match = key.match(/^(.+)_([^_]+)_ACCESS_KEY$/); if (match && match.length >= 3) { const apiType = match[1].toLowerCase(); const providerName = match[2].toLowerCase(); const providerKey = `${apiType}_${providerName}`; ``` -------------------------------- ### Show Toast Convenience Functions Source: https://github.com/p32929/rotato/blob/master/public/admin.html Provides simple wrapper functions for displaying different types of toasts (success, error, warning, info) with default durations. These functions abstract the underlying `showToast` call. ```javascript function showSuccessToast(message, duration = 3000) { return showToast(message, 'success', duration); } function showErrorToast(message, duration = 5000) { return showToast(message, 'error', duration); } function showWarningToast(message, duration = 4000) { return showToast(message, 'warning', duration); } function showInfoToast(message, duration = 3000) { return showToast(message, 'info', duration); } ``` -------------------------------- ### Test New Provider API Key Source: https://github.com/p32929/rotato/blob/master/public/admin.html Tests a newly entered API key by sending it to the server for validation. Provides feedback on whether the key is valid or invalid. ```javascript async function testNewProviderKey(keyIndex) { const apiType = document.getElementById('newProviderApiType').value; const baseUrl = document.getElementById('newProviderBaseUrl').value.trim(); const keyInput = document.querySelector(`[data-key-index="${keyIndex}"] input`); const apiKey = keyInput.value.trim(); if (!apiKey) { showWarningToast('Please enter an API key to test'); return; } const maskedKey = apiKey.length > 16 ? apiKey.substring(0, 8) + '...' + apiKey.slice(-4) : apiKey; showInfoToast(`Testing new API key ${maskedKey}...`); try { const response = await fetch('/admin/api/test', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ apiType: apiType, apiKey: apiKey, baseUrl: baseUrl }) }); const result = await response.json(); if (result.success) { showSuccessToast(`✅ API key ${maskedKey} is valid! Ready to save.`); } else { showErrorToast(`❌ API key ${maskedKey} is invalid: ${result.error || 'Unknown error'}`); } } catch (error) { showErrorToast(`❌ API key test failed: ${error.message}`); } } ``` -------------------------------- ### Parse API Configuration Keys Source: https://github.com/p32929/rotato/blob/master/public/admin.html Parses environment keys to configure API access, default models, and model history for different providers. It dynamically creates or updates provider objects based on key naming conventions. ```javascript `${apiType}_${providerName}`; if (!providers[providerKey]) { providers[providerKey] = { name: providerName, apiType: apiType, keys: [], baseUrl: '', accessKey: '', defaultModel: '', modelHistory: [] }; } providers[providerKey].accessKey = value; } else if (key.endsWith('_DEFAULT_MODEL') && value) { // Extract API_TYPE and PROVIDER from key const parts = key.replace('_DEFAULT_MODEL', '').split('_'); if (parts.length < 2) continue; const apiType = parts[0].toLowerCase(); const providerName = parts.slice(1).join('_').toLowerCase(); const providerKey = `${apiType}_${providerName}`; if (!providers[providerKey]) { providers[providerKey] = { name: providerName, apiType: apiType, keys: [], baseUrl: '', accessKey: '', defaultModel: '', modelHistory: [] }; } providers[providerKey].defaultModel = value; } else if (key.endsWith('_MODEL_HISTORY') && value) { // Extract API_TYPE and PROVIDER from key const parts = key.replace('_MODEL_HISTORY', '').split('_'); if (parts.length < 2) continue; const apiType = parts[0].toLowerCase(); const providerName = parts.slice(1).join('_').toLowerCase(); const providerKey = `${apiType}_${providerName}`; if (!providers[providerKey]) { providers[providerKey] = { name: providerName, apiType: apiType, keys: [], baseUrl: '', accessKey: '', defaultModel: '', modelHistory: [] }; } providers[providerKey].modelHistory = value.split(',').map(m => m.trim()).filter(m => m); } ``` -------------------------------- ### Render Provider UI Elements Source: https://github.com/p32929/rotato/blob/master/public/admin.html Dynamically generates HTML elements for each provider, including buttons for managing access keys and default models. The button states (enabled/disabled, icons) reflect whether values are present. ```javascript const sortedProviders = Object.values(providers).sort((a, b) => (a.disabled ? 1 : 0) - (b.disabled ? 1 : 0)); sortedProviders.forEach((provider, index) => { const providerDiv = document.createElement('div'); providerDiv.className = 'key-row'; // Determine initial button state for access key let accessKeyButtonHtml, accessKeyAction, accessKeyClass, accessKeyStyle, accessKeyTitle; if (provider.accessKey) { // Has value - show delete icon accessKeyButtonHtml = ` `; accessKeyAction = `deleteAccessKey('${provider.apiType}', '${provider.name}')`; accessKeyClass = 'text-muted-foreground hover:text-foreground p-1'; accessKeyStyle = ''; accessKeyTitle = 'Delete'; } else { // Empty - show disabled save icon accessKeyButtonHtml = ` `; accessKeyAction = ''; // No action when disabled accessKeyClass = 'cursor-not-allowed p-1'; accessKeyStyle = 'pointer-events: none; opacity: 0.3;'; accessKeyTitle = 'Nothing to save'; } // Determine initial button state for default model let defaultModelButtonHtml, defaultModelAction, defaultModelClass, defaultModelStyle, defaultModelTitle; if (provider.defaultModel) { // Has value - show delete icon defaultModelButtonHtml = ` `; defaultModelAction = `deleteDefaultModel('${provider.apiType}', '${provider.name}')`; defaultModelClass = 'absolute right-2 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground p-1'; defaultModelStyle = ''; defaultModelTitle = 'Delete'; } else { // Empty - show disabled save icon defaultModelButtonHtml = ` `; defaultModelAction = ''; // No action when disabled defaultModelClass = 'absolute right-2 top-1/2 -translate-y-1/2 cursor-not-allowed p-1'; defaultModelStyle = 'pointer-events: none; opacity: 0.3;'; defaultModelTitle = 'Nothing to save'; } }); ``` -------------------------------- ### Call Gemini-Compatible API Source: https://github.com/p32929/rotato/blob/master/README.md This cURL command demonstrates how to send a content generation request to the Gemini-compatible API endpoint. Replace '[ACCESS_KEY:your-access-key]' with your actual API key if it's configured. ```bash curl -X POST "http://localhost:8990/gemini/models/gemini-2.5-flash:generateContent" \ -H "x-goog-api-key: [STATUS_CODES:429][ACCESS_KEY:your-access-key]" \ -H "Content-Type: application/json" \ -d '{ "contents": [ { "parts": [ { "text": "Hello! Please say hello back." } ] } ]' }' ``` -------------------------------- ### Render API Keys for a Provider Source: https://github.com/p32929/rotato/blob/master/public/admin.html Dynamically renders the list of API keys for a given provider, including their status, masked value, and usage count. It also provides controls for managing the keys. ```javascript providersContainer.appendChild(providerDiv); }); if (Object.keys(providers).length === 0) { providersContainer.innerHTML = '
No providers configured yet.' } ``` -------------------------------- ### Call OpenAI-Compatible API Source: https://github.com/p32929/rotato/blob/master/README.md Use this cURL command to send a chat completion request to the OpenAI-compatible API endpoint. Ensure you replace '[ACCESS_KEY:your-access-key]' with your actual access key if configured. ```bash curl -X POST "http://localhost:8990/groq/chat/completions" \ -H "Authorization: Bearer [STATUS_CODES:429][ACCESS_KEY:your-access-key]" \ -H "Content-Type: application/json" \ -d '{ "model": "openai/gpt-oss-120b", "messages": [ { "role": "user", "content": "Hello! Please say hello back." } ] }' ``` -------------------------------- ### API Key Management UI Elements Source: https://github.com/p32929/rotato/blob/master/public/admin.html Generates HTML for displaying and managing individual API keys within a provider's configuration. Includes toggles, reorder buttons, and action buttons for testing and deletion. ```html
${maskedKey}
${usageDisplay} uses
``` -------------------------------- ### Login with Enter Key Source: https://github.com/p32929/rotato/blob/master/public/admin.html Attaches an event listener to the password input field to trigger the `login()` function when the 'Enter' key is pressed. ```javascript // Event listeners document.getElementById('password').addEventListener('keypress', function(e) { if (e.key === 'Enter') { login(); } }); ``` -------------------------------- ### Show Delete Confirmation Dialog Source: https://github.com/p32929/rotato/blob/master/public/admin.html Displays a confirmation dialog before deleting an API key. Sets up the confirm button to trigger the actual deletion function. ```javascript function showDeleteKeyConfirmation(apiType, providerName, keyIndex) { const dialog = document.getElementById('confirmDialog'); const message = document.getElementById('confirmMessage'); message.textContent = `Are you sure you want to delete this API key from provider '${providerName}'? This action cannot be undone.`; const confirmBtn = dialog.querySelector('button[onclick="confirmDelete()"]'); confirmBtn.onclick = () => { dialog.classList.add('hidden'); deleteProviderKey(apiType, providerName, keyIndex); }; dialog.classList.remove('hidden'); } ``` -------------------------------- ### Render Provider Information in Admin Panel Source: https://github.com/p32929/rotato/blob/master/public/admin.html This JavaScript code dynamically generates the HTML for a provider's section in the admin panel. It calculates key counts, determines the provider's disabled status, and constructs the UI elements for toggling, renaming, copying cURL commands, and deleting the provider. It also displays configuration details like base URLs. ```javascript const enabledKeyCount = provider.keys.filter(k => !k.disabled).length; const totalKeyCount = provider.keys.length; const isProviderDisabled = provider.disabled || false; providerDiv.innerHTML =

${provider.name}${isProviderDisabled ? ' ' : ''}

${provider.apiType.toUpperCase()} Compatible • ${enabledKeyCount}/${totalKeyCount} keys active

Configuration
${provider.baseUrl || 'Using Default URL'}
${window.location.protocol}//${window.location.host}/${provider.name}*
``` -------------------------------- ### Save Environment Variables Source: https://github.com/p32929/rotato/blob/master/public/admin.html Saves the current state of environment variables to the server. Reloads configuration upon successful save. ```javascript async function saveEnvVars() { try { const response = await fetch('/admin/api/env', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(envVars) }); if (response.ok) { showSuccess('envSuccess', 'Environment variables saved and configuration reloaded!'); } else { showError('envError', 'Failed to save environment variables'); } } catch (error) { showError('envError', 'Save failed: ' + error.message); } } ``` -------------------------------- ### Initiate Provider Rename Source: https://github.com/p32929/rotato/blob/master/public/admin.html Replaces the provider name display with an inline input field and save/cancel buttons for renaming. Focuses the input field automatically. ```javascript function startRenameProvider(apiType, providerName) { const nameDiv = document.getElementById(`provider-name-${apiType}-${providerName}`); if (!nameDiv) return; const currentName = providerName; nameDiv.innerHTML = `
`; setTimeout(() => { const input = document.getElementById(`rename-input-${apiType}-${providerName}`); if (input) { input.focus(); input.select(); } }, 0); } ``` -------------------------------- ### Update Provider Preview Dynamically Source: https://github.com/p32929/rotato/blob/master/public/admin.html Updates a preview element with provider details as the user inputs them. Includes validation for missing required fields. ```javascript function updateProviderPreview() { const name = document.getElementById('newProviderName').value.trim(); const apiType = document.getElementById('newProviderApiType').value; const baseUrl = document.getElementById('newProviderBaseUrl').value.trim(); const previewElement = document.getElementById('previewText'); if (!previewElement) return; const defaultConfig = getDefaultProviderConfig(apiType); if (!defaultConfig) { previewElement.textContent = 'Unknown API type'; return; } const hasName = name.length > 0; const hasBaseUrl = baseUrl.length > 0; // Check for validation errors if (hasName && !hasBaseUrl) { previewElement.innerHTML = `⚠️ Validation Error: Provider name provided but base URL is missing. Please provide both or leave both empty for default.`; previewElement.parentElement.style.borderColor = 'var(--destructive)'; previewElement.parentElement.style.backgroundColor = 'rgb(239 68 68 / 0.1)'; return; } if (!hasName && hasBaseUrl) { previewElement.innerHTML = `⚠️ Validation Error: Base URL provided but provider name is missing. ` } } ``` -------------------------------- ### Load Telegram Bot Settings Source: https://github.com/p32929/rotato/blob/master/public/admin.html Asynchronously fetches and populates Telegram bot settings from the server. Updates the UI to reflect the bot's running status. ```javascript async function loadTelegramSettings() { try { const response = await fetch('/admin/api/telegram'); if (!response.ok) return; const data = await response.json(); document.getElementById('telegramBotToken').value = data.botToken || ''; document.getElementById('telegramAllowedUsers').value = data.allowedUsers || ''; document.getElementById('defaultStatusCodes').value = data.defaultStatusCodes || '429'; document.getElementById('keepAliveMinutes').value = data.keepAliveMinutes != null ? data.keepAliveMinutes : 10; const statusEl = document.getElementById('telegramBotStatus'); if (data.botRunning) { statusEl.innerHTML = 'Bot is running'; } else if (data.botToken) { statusEl.innerHTML = 'Bot token set but not running'; } else { statusEl.innerHTML = 'Bot not configured'; } } catch (err) { console.error('Failed to load telegram settings:', err); } } ``` -------------------------------- ### Show Delete Provider Confirmation Dialog Source: https://github.com/p32929/rotato/blob/master/public/admin.html Displays a confirmation dialog to the user before deleting a provider. Informs the user about the consequences of the action. ```javascript function showDeleteProviderConfirmation(apiType, providerName) { const dialog = document.getElementById('confirmDialog'); const message = document.getElementById('confirmMessage'); message.textContent = `Delete provider '${providerName}'? This will remove all its API keys and configuration. This action cannot be undone.`; // Set up the confirm ```