### 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
```
--------------------------------
### 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
```