Revamp the dashboard interface

parent 59a08356
...@@ -2,25 +2,21 @@ ...@@ -2,25 +2,21 @@
A modular proxy server for managing multiple AI provider integrations with unified API interface. AISBF provides intelligent routing, load balancing, and AI-assisted model selection to optimize AI service usage across multiple providers. A modular proxy server for managing multiple AI provider integrations with unified API interface. AISBF provides intelligent routing, load balancing, and AI-assisted model selection to optimize AI service usage across multiple providers.
## 🚀 Support AISBF - Your Donations Matter! ---
The project includes multiple donation options to support its development: ## 🌐 Try AISBF Now — No Installation Required!
### Ethereum Donation > **[➡️ Launch AISBF at https://aisbf.cloud](https://aisbf.cloud)**
ETH to `0xdA6dAb526515b5cb556d20269207D43fcc760E51` >
> The fully hosted service is free to use. Just open your browser and start routing AI requests across all supported providers — no setup, no configuration, no API keys needed to get started.
### PayPal Donation Also available via TOR for privacy-first access:
https://paypal.me/nexlab [http://aisbfity4ud6nsht53tsh2iauaur2e4dah2gplcprnikyjpkg72vfjad.onion](http://aisbfity4ud6nsht53tsh2iauaur2e4dah2gplcprnikyjpkg72vfjad.onion)
### Bitcoin Donation
Address: `bc1qcpt2uutqkz4456j5r78rjm3gwq03h5fpwmcc5u`
## Try AISBF
Try AISBF live at [https://aisbf.cloud](https://aisbf.cloud) or via TOR at [http://aisbfity4ud6nsht53tsh2iauaur2e4dah2gplcprnikyjpkg72vfjad.onion](http://aisbfity4ud6nsht53tsh2iauaur2e4dah2gplcprnikyjpkg72vfjad.onion) - no installation required!
![AISBF Dashboard](https://git.nexlab.net/nexlab/aisbf/raw/master/screenshot.png) ![AISBF Dashboard](https://git.nexlab.net/nexlab/aisbf/raw/master/screenshot.png)
---
## Key Features ## Key Features
- **Multi-Provider Support**: Unified interface for Google, OpenAI, Anthropic, Claude Code (OAuth2), Ollama, Kiro, Kilocode, Codex, and Qwen - **Multi-Provider Support**: Unified interface for Google, OpenAI, Anthropic, Claude Code (OAuth2), Ollama, Kiro, Kilocode, Codex, and Qwen
...@@ -103,6 +99,19 @@ For complete documentation, configuration guides, and API reference: ...@@ -103,6 +99,19 @@ For complete documentation, configuration guides, and API reference:
- **[🔌 API Reference](DOCUMENTATION.md#api-endpoints)** - Complete API documentation - **[🔌 API Reference](DOCUMENTATION.md#api-endpoints)** - Complete API documentation
- **[🛠️ Development](DOCUMENTATION.md#development)** - Development and deployment guides - **[🛠️ Development](DOCUMENTATION.md#development)** - Development and deployment guides
## 🚀 Support AISBF - Your Donations Matter!
The project includes multiple donation options to support its development:
### Ethereum Donation
ETH to `0xdA6dAb526515b5cb556d20269207D43fcc760E51`
### PayPal Donation
https://paypal.me/nexlab
### Bitcoin Donation
Address: `bc1qcpt2uutqkz4456j5r78rjm3gwq03h5fpwmcc5u`
## Author ## Author
Stefy Lanza <stefy@nexlab.net> Stefy Lanza <stefy@nexlab.net>
......
...@@ -2291,7 +2291,11 @@ async def dashboard_analytics( ...@@ -2291,7 +2291,11 @@ async def dashboard_analytics(
user_filter_int = current_user_id user_filter_int = current_user_id
# Get all users for filter dropdown (only for admins) # Get all users for filter dropdown (only for admins)
all_users = db.get_users() if db and is_admin else [] raw_users = db.get_users() if db and is_admin else []
all_users = [
{k: (v.isoformat() if isinstance(v, datetime) else v) for k, v in u.items()}
for u in raw_users
]
# Get available providers, models, rotations, and autoselects for filter dropdowns # Get available providers, models, rotations, and autoselects for filter dropdowns
available_providers = list(config.providers.keys()) if config else [] available_providers = list(config.providers.keys()) if config else []
...@@ -2379,7 +2383,8 @@ async def dashboard_analytics( ...@@ -2379,7 +2383,8 @@ async def dashboard_analytics(
"selected_rotation": rotation_filter, "selected_rotation": rotation_filter,
"selected_autoselect": autoselect_filter, "selected_autoselect": autoselect_filter,
"selected_user": user_filter, "selected_user": user_filter,
"global_only": global_only "global_only": global_only,
"currency_symbol": DatabaseRegistry.get_config_database().get_currency_settings().get('currency_symbol', '$')
} }
) )
...@@ -2500,6 +2505,7 @@ async def dashboard_login(request: Request, username: str = Form(...), password: ...@@ -2500,6 +2505,7 @@ async def dashboard_login(request: Request, username: str = Form(...), password:
logger.info(f"User authenticated: username={username}, email={user.get('email')}, user_id={user['id']}") logger.info(f"User authenticated: username={username}, email={user.get('email')}, user_id={user['id']}")
request.session['logged_in'] = True request.session['logged_in'] = True
request.session['username'] = username request.session['username'] = username
request.session['display_name'] = user.get('display_name') or ''
request.session['email'] = user.get('email') or '' # Ensure we get the email from user dict request.session['email'] = user.get('email') or '' # Ensure we get the email from user dict
request.session['role'] = user['role'] request.session['role'] = user['role']
request.session['user_id'] = user['id'] request.session['user_id'] = user['id']
...@@ -3203,8 +3209,9 @@ async def dashboard_profile_save(request: Request, username: str = Form(...), di ...@@ -3203,8 +3209,9 @@ async def dashboard_profile_save(request: Request, username: str = Form(...), di
try: try:
db.update_user_profile(user_id, username, None, display_name if display_name else None) db.update_user_profile(user_id, username, None, display_name if display_name else None)
# Update session with new username # Update session with new username and display_name
request.session['username'] = username request.session['username'] = username
request.session['display_name'] = display_name or ''
return RedirectResponse(url=url_for(request, "/dashboard/profile?success=Profile updated successfully"), status_code=303) return RedirectResponse(url=url_for(request, "/dashboard/profile?success=Profile updated successfully"), status_code=303)
except Exception as e: except Exception as e:
...@@ -4031,7 +4038,8 @@ async def dashboard_index(request: Request): ...@@ -4031,7 +4038,8 @@ async def dashboard_index(request: Request):
"subscription": subscription, "subscription": subscription,
"current_tier": current_tier, "current_tier": current_tier,
"payment_methods": payment_methods, "payment_methods": payment_methods,
"currency_symbol": currency_symbol "currency_symbol": currency_symbol,
"display_name": (db.get_user_by_id(user_id) or {}).get('display_name') or request.session.get('username', '') if user_id else request.session.get('username', '')
} }
) )
...@@ -8047,7 +8055,8 @@ async def dashboard_admin_payment_settings(request: Request): ...@@ -8047,7 +8055,8 @@ async def dashboard_admin_payment_settings(request: Request):
name="dashboard/admin_payment_settings.html", name="dashboard/admin_payment_settings.html",
context={ context={
"request": request, "request": request,
"session": request.session "session": request.session,
"currency_symbol": DatabaseRegistry.get_config_database().get_currency_settings().get('currency_symbol', '$')
} }
) )
...@@ -8146,11 +8155,16 @@ async def dashboard_wallet(request: Request): ...@@ -8146,11 +8155,16 @@ async def dashboard_wallet(request: Request):
all_gateways = db.get_payment_gateway_settings() all_gateways = db.get_payment_gateway_settings()
enabled_gateways = {k: v for k, v in all_gateways.items() if v.get('enabled', False)} enabled_gateways = {k: v for k, v in all_gateways.items() if v.get('enabled', False)}
return templates.TemplateResponse("dashboard/wallet.html", { return templates.TemplateResponse(
"request": request, request=request,
"wallet": wallet, name="dashboard/wallet.html",
"enabled_gateways": enabled_gateways, context={
}) "request": request,
"wallet": wallet,
"enabled_gateways": enabled_gateways,
"currency_symbol": db.get_currency_settings().get('currency_symbol', '$'),
}
)
except ImportError: except ImportError:
return HTMLResponse("Wallet functionality not available", status_code=503) return HTMLResponse("Wallet functionality not available", status_code=503)
except Exception as e: except Exception as e:
...@@ -8329,7 +8343,8 @@ async def dashboard_billing(request: Request): ...@@ -8329,7 +8343,8 @@ async def dashboard_billing(request: Request):
"payment_methods": payment_methods, "payment_methods": payment_methods,
"transactions": transactions, "transactions": transactions,
"enabled_gateways": enabled_gateways, "enabled_gateways": enabled_gateways,
"wallet": wallet "wallet": wallet,
"currency_symbol": currency_settings.get('currency_symbol', '$')
} }
) )
......
This diff is collapsed.
...@@ -140,7 +140,7 @@ document.addEventListener('DOMContentLoaded', function() { ...@@ -140,7 +140,7 @@ document.addEventListener('DOMContentLoaded', function() {
if (data.success) { if (data.success) {
window.location.href = '/dashboard/billing?success=' + encodeURIComponent(data.message); window.location.href = '/dashboard/billing?success=' + encodeURIComponent(data.message);
} else { } else {
alert('Error: ' + data.error); showAlert('Error: ' + data.error, 'Error', '❌', 'danger');
} }
}); });
}); });
...@@ -165,7 +165,7 @@ document.addEventListener('DOMContentLoaded', function() { ...@@ -165,7 +165,7 @@ document.addEventListener('DOMContentLoaded', function() {
const stripeKey = '{{ stripe_publishable_key }}'; const stripeKey = '{{ stripe_publishable_key }}';
if (!stripeKey) { if (!stripeKey) {
alert('Stripe is not configured. Please contact the administrator.'); showAlert('Stripe is not configured. Please contact the administrator.', 'Not Configured', '⚠️', 'warn');
return; return;
} }
......
...@@ -19,6 +19,49 @@ ...@@ -19,6 +19,49 @@
</div> </div>
</div> </div>
<!-- Global Currency Settings -->
<div style="background: #16213e; border: 2px solid #17a2b8; border-radius: 8px; padding: 20px; margin-bottom: 20px;">
<h3 style="margin: 0 0 20px 0; color: #17a2b8;">
<i class="fas fa-dollar-sign me-2"></i>Global Currency Settings
</h3>
<form id="currencySettingsForm">
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 15px; margin-bottom: 20px;">
<div>
<label style="display: block; margin-bottom: 5px; font-weight: 500; color: #e0e0e0;">Currency Code</label>
<select style="width: 100%; padding: 8px; border: 1px solid #0f3460; border-radius: 3px; font-size: 14px; background: #1a1a2e; color: #e0e0e0;" id="currencyCode">
<option value="USD">USD - US Dollar</option>
<option value="EUR">EUR - Euro</option>
<option value="GBP">GBP - British Pound</option>
<option value="JPY">JPY - Japanese Yen</option>
<option value="CAD">CAD - Canadian Dollar</option>
<option value="AUD">AUD - Australian Dollar</option>
<option value="CHF">CHF - Swiss Franc</option>
<option value="CNY">CNY - Chinese Yuan</option>
<option value="INR">INR - Indian Rupee</option>
<option value="BRL">BRL - Brazilian Real</option>
</select>
</div>
<div>
<label style="display: block; margin-bottom: 5px; font-weight: 500; color: #e0e0e0;">Currency Symbol</label>
<input type="text" style="width: 100%; padding: 8px; border: 1px solid #0f3460; border-radius: 3px; font-size: 14px; background: #1a1a2e; color: #e0e0e0;" id="currencySymbol" value="{{ currency_symbol }}">
</div>
<div>
<label style="display: block; margin-bottom: 5px; font-weight: 500; color: #e0e0e0;">Decimal Places</label>
<select style="width: 100%; padding: 8px; border: 1px solid #0f3460; border-radius: 3px; font-size: 14px; background: #1a1a2e; color: #e0e0e0;" id="currencyDecimals">
<option value="0">0</option>
<option value="1">1</option>
<option value="2" selected>2</option>
<option value="3">3</option>
</select>
</div>
</div>
<button type="submit" class="btn" style="background: #17a2b8; color: white;">
<i class="fas fa-save me-2"></i>Save Currency Settings
</button>
</form>
</div>
<!-- Encryption Key Configuration --> <!-- Encryption Key Configuration -->
<div style="background: #16213e; border: 2px solid #e74c3c; border-radius: 8px; padding: 20px; margin-bottom: 20px;"> <div style="background: #16213e; border: 2px solid #e74c3c; border-radius: 8px; padding: 20px; margin-bottom: 20px;">
<h3 style="margin: 0 0 20px 0; color: #e74c3c;"> <h3 style="margin: 0 0 20px 0; color: #e74c3c;">
...@@ -638,9 +681,8 @@ async function saveEncryptionKey() { ...@@ -638,9 +681,8 @@ async function saveEncryptionKey() {
return; return;
} }
if (!confirm('WARNING: Setting or changing the encryption key will affect all encrypted data. Are you sure?')) { const ok = await showDangerConfirm('WARNING: Setting or changing the encryption key will affect all encrypted data. Are you sure?', 'Change Encryption Key');
return; if (!ok) return;
}
try { try {
const response = await fetch('{{ url_for(request, "/api/admin/settings/encryption-key") }}', { const response = await fetch('{{ url_for(request, "/api/admin/settings/encryption-key") }}', {
...@@ -678,7 +720,7 @@ async function loadSystemStatus() { ...@@ -678,7 +720,7 @@ async function loadSystemStatus() {
} }
// Update balances // Update balances
document.getElementById('totalBalance').textContent = '$' + (data.total_balance_usd || 0).toFixed(2); document.getElementById('totalBalance').textContent = '{{ currency_symbol }}' + (data.total_balance_usd || 0).toFixed(2);
document.getElementById('pendingPayments').textContent = data.pending_payments || 0; document.getElementById('pendingPayments').textContent = data.pending_payments || 0;
document.getElementById('failedPayments').textContent = data.failed_payments || 0; document.getElementById('failedPayments').textContent = data.failed_payments || 0;
} catch (error) { } catch (error) {
...@@ -1062,6 +1104,7 @@ document.addEventListener('DOMContentLoaded', function() { ...@@ -1062,6 +1104,7 @@ document.addEventListener('DOMContentLoaded', function() {
loadConfiguration(); loadConfiguration();
loadPaymentGateways(); loadPaymentGateways();
loadCryptoPrices(); loadCryptoPrices();
loadCurrencySettings();
// Auto-refresh crypto prices every 60 seconds // Auto-refresh crypto prices every 60 seconds
setInterval(loadCryptoPrices, 60000); setInterval(loadCryptoPrices, 60000);
...@@ -1139,6 +1182,41 @@ function updateCryptoPrice(crypto, priceData, currencySymbol) { ...@@ -1139,6 +1182,41 @@ function updateCryptoPrice(crypto, priceData, currencySymbol) {
} }
} }
function loadCurrencySettings() {
fetch('{{ url_for(request, "/api/admin/settings/currency") }}')
.then(response => response.json())
.then(settings => {
document.getElementById('currencyCode').value = settings.currency_code;
document.getElementById('currencySymbol').value = settings.currency_symbol;
document.getElementById('currencyDecimals').value = settings.currency_decimals;
});
}
document.getElementById('currencySettingsForm').addEventListener('submit', function(e) {
e.preventDefault();
const currencyData = {
currency_code: document.getElementById('currencyCode').value,
currency_symbol: document.getElementById('currencySymbol').value,
currency_decimals: parseInt(document.getElementById('currencyDecimals').value)
};
fetch('{{ url_for(request, "/api/admin/settings/currency") }}', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(currencyData)
})
.then(response => response.json())
.then(result => {
showToast('Currency settings saved successfully', 'success');
})
.catch(error => {
showToast('Error saving currency settings', 'danger');
});
});
// Refresh crypto prices manually // Refresh crypto prices manually
function refreshCryptoPrices() { function refreshCryptoPrices() {
['Btc', 'Eth', 'Usdt', 'Usdc'].forEach(crypto => { ['Btc', 'Eth', 'Usdt', 'Usdc'].forEach(crypto => {
......
...@@ -69,49 +69,6 @@ ...@@ -69,49 +69,6 @@
</div> </div>
</div> </div>
<!-- Global Currency Settings -->
<div style="background: #16213e; border: 2px solid #17a2b8; border-radius: 8px; padding: 20px; margin-bottom: 20px;">
<h3 style="margin: 0 0 20px 0; color: #17a2b8;">
<i class="fas fa-dollar-sign me-2"></i>Global Currency Settings
</h3>
<form id="currencySettingsForm">
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 15px; margin-bottom: 20px;">
<div>
<label style="display: block; margin-bottom: 5px; font-weight: 500; color: #e0e0e0;">Currency Code</label>
<select style="width: 100%; padding: 8px; border: 1px solid #0f3460; border-radius: 3px; font-size: 14px; background: #1a1a2e; color: #e0e0e0;" id="currencyCode">
<option value="USD">USD - US Dollar</option>
<option value="EUR">EUR - Euro</option>
<option value="GBP">GBP - British Pound</option>
<option value="JPY">JPY - Japanese Yen</option>
<option value="CAD">CAD - Canadian Dollar</option>
<option value="AUD">AUD - Australian Dollar</option>
<option value="CHF">CHF - Swiss Franc</option>
<option value="CNY">CNY - Chinese Yuan</option>
<option value="INR">INR - Indian Rupee</option>
<option value="BRL">BRL - Brazilian Real</option>
</select>
</div>
<div>
<label style="display: block; margin-bottom: 5px; font-weight: 500; color: #e0e0e0;">Currency Symbol</label>
<input type="text" style="width: 100%; padding: 8px; border: 1px solid #0f3460; border-radius: 3px; font-size: 14px; background: #1a1a2e; color: #e0e0e0;" id="currencySymbol" value="$">
</div>
<div>
<label style="display: block; margin-bottom: 5px; font-weight: 500; color: #e0e0e0;">Decimal Places</label>
<select style="width: 100%; padding: 8px; border: 1px solid #0f3460; border-radius: 3px; font-size: 14px; background: #1a1a2e; color: #e0e0e0;" id="currencyDecimals">
<option value="0">0</option>
<option value="1">1</option>
<option value="2" selected>2</option>
<option value="3">3</option>
</select>
</div>
</div>
<button type="submit" class="btn" style="background: #17a2b8; color: white;">
<i class="fas fa-save me-2"></i>Save Currency Settings
</button>
</form>
</div>
<!-- Create/Edit Tier Modal --> <!-- Create/Edit Tier Modal -->
<div class="modal fade" id="tierModal" tabindex="-1" aria-labelledby="tierModalLabel" aria-hidden="true" style="display: none;"> <div class="modal fade" id="tierModal" tabindex="-1" aria-labelledby="tierModalLabel" aria-hidden="true" style="display: none;">
...@@ -229,7 +186,6 @@ ...@@ -229,7 +186,6 @@
<script> <script>
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function() {
try { try {
loadCurrencySettings();
} catch (error) { } catch (error) {
console.error('Error initializing page:', error); console.error('Error initializing page:', error);
} }
...@@ -403,15 +359,15 @@ function closeTierModal() { ...@@ -403,15 +359,15 @@ function closeTierModal() {
document.getElementById('tierModal').style.display = 'none'; document.getElementById('tierModal').style.display = 'none';
} }
function deleteTier(tierId) { async function deleteTier(tierId) {
if (confirm('Are you sure you want to delete this tier? Users on this tier will be moved to the default free tier.')) { const ok = await showDangerConfirm('Are you sure you want to delete this tier? Users on this tier will be moved to the default free tier.', 'Delete Tier');
if (ok) {
fetch('{{ url_for(request, "/api/admin/tiers") }}/' + tierId, { fetch('{{ url_for(request, "/api/admin/tiers") }}/' + tierId, {
method: 'DELETE' method: 'DELETE'
}) })
.then(response => response.json()) .then(response => response.json())
.then(result => { .then(result => {
showToast('Tier deleted successfully', 'success'); showToast('Tier deleted successfully', 'success');
// Reload page to reflect changes
window.location.reload(); window.location.reload();
}) })
.catch(error => { .catch(error => {
...@@ -420,40 +376,6 @@ function deleteTier(tierId) { ...@@ -420,40 +376,6 @@ function deleteTier(tierId) {
} }
} }
function loadCurrencySettings() {
fetch('{{ url_for(request, "/api/admin/settings/currency") }}')
.then(response => response.json())
.then(settings => {
document.getElementById('currencyCode').value = settings.currency_code;
document.getElementById('currencySymbol').value = settings.currency_symbol;
document.getElementById('currencyDecimals').value = settings.currency_decimals;
});
}
document.getElementById('currencySettingsForm').addEventListener('submit', function(e) {
e.preventDefault();
const currencyData = {
currency_code: document.getElementById('currencyCode').value,
currency_symbol: document.getElementById('currencySymbol').value,
currency_decimals: parseInt(document.getElementById('currencyDecimals').value)
};
fetch('{{ url_for(request, "/api/admin/settings/currency") }}', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(currencyData)
})
.then(response => response.json())
.then(result => {
showToast('Currency settings saved successfully', 'success');
})
.catch(error => {
showToast('Error saving currency settings', 'danger');
});
});
function showToast(message, type) { function showToast(message, type) {
......
...@@ -99,7 +99,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. ...@@ -99,7 +99,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
</span> </span>
{% if date_range_usage %} {% if date_range_usage %}
<span style="margin-left: 20px; color: #a0a0a0;"> <span style="margin-left: 20px; color: #a0a0a0;">
| Total: {{ format_tokens(date_range_usage.total_tokens) }} tokens | Estimated Cost: ${{ "%.2f"|format(date_range_usage.estimated_cost) }} | Total: {{ format_tokens(date_range_usage.total_tokens) }} tokens | Estimated Cost: {{ currency_symbol }}{{ "%.2f"|format(date_range_usage.estimated_cost) }}
</span> </span>
{% endif %} {% endif %}
</div> </div>
...@@ -494,7 +494,7 @@ document.getElementById('timeRangeSelect').addEventListener('change', function() ...@@ -494,7 +494,7 @@ document.getElementById('timeRangeSelect').addEventListener('change', function()
{% if total_requests > 0 %} {% if total_requests > 0 %}
{% set weighted_sum = namespace(value=0) %} {% set weighted_sum = namespace(value=0) %}
{% for provider in provider_stats %} {% for provider in provider_stats %}
{% set weighted_sum.value = weighted_sum.value + (provider.avg_latency_ms * provider.requests.total) %} {% set weighted_sum.value = weighted_sum.value + (provider.avg_latency_ms | float * provider.requests.total) %}
{% endfor %} {% endfor %}
{% set avg_latency = weighted_sum.value / total_requests %} {% set avg_latency = weighted_sum.value / total_requests %}
{% if avg_latency > 1000 %}{{ "%.1f"|format(avg_latency / 1000) }}s{% else %}{{ "%.0f"|format(avg_latency) }}ms{% endif %} {% if avg_latency > 1000 %}{{ "%.1f"|format(avg_latency / 1000) }}s{% else %}{{ "%.0f"|format(avg_latency) }}ms{% endif %}
...@@ -521,7 +521,7 @@ document.getElementById('timeRangeSelect').addEventListener('change', function() ...@@ -521,7 +521,7 @@ document.getElementById('timeRangeSelect').addEventListener('change', function()
<h4 style="font-size: 14px; margin-bottom: 10px;"> <h4 style="font-size: 14px; margin-bottom: 10px;">
{% if from_date or to_date %}Selected Period Cost{% else %}Today's Estimated Cost{% endif %} {% if from_date or to_date %}Selected Period Cost{% else %}Today's Estimated Cost{% endif %}
</h4> </h4>
<p style="font-size: 28px; font-weight: bold;">${{ "%.2f"|format(cost_overview.total_estimated_cost_today) }}</p> <p style="font-size: 28px; font-weight: bold;">{{ currency_symbol }}{{ "%.2f"|format(cost_overview.total_estimated_cost_today) }}</p>
{% if cost_overview.date_range %} {% if cost_overview.date_range %}
<small style="color: rgba(255,255,255,0.8);"> <small style="color: rgba(255,255,255,0.8);">
{{ cost_overview.date_range.start[:10] }} to {{ cost_overview.date_range.end[:10] }} {{ cost_overview.date_range.start[:10] }} to {{ cost_overview.date_range.end[:10] }}
......
...@@ -315,14 +315,14 @@ function renderAutoselectModels(autoselectKey) { ...@@ -315,14 +315,14 @@ function renderAutoselectModels(autoselectKey) {
}); });
} }
function addAutoselect() { async function addAutoselect() {
const key = prompt('Enter autoselect key (e.g., "autoselect", "smart-select"):'); const key = await showPrompt('Enter autoselect key (e.g., "autoselect", "smart-select"):', '', 'e.g. autoselect', 'Add Autoselect');
if (!key) { if (!key) {
return; return;
} }
if (autoselectConfig[key]) { if (autoselectConfig[key]) {
alert('Autoselect key already exists'); showAlert('Autoselect key already exists', 'Duplicate Key', '⚠️', 'warn');
return; return;
} }
...@@ -338,8 +338,8 @@ function addAutoselect() { ...@@ -338,8 +338,8 @@ function addAutoselect() {
renderAutoselectList(); renderAutoselectList();
} }
function removeAutoselect(key) { async function removeAutoselect(key) {
if (confirm(`Remove autoselect "${key}"?`)) { if (await showDangerConfirm(`Remove autoselect "${key}"?`, 'Remove Autoselect')) {
delete autoselectConfig[key]; delete autoselectConfig[key];
expandedAutoselects.delete(key); expandedAutoselects.delete(key);
renderAutoselectList(); renderAutoselectList();
...@@ -375,8 +375,8 @@ function addAutoselectModel(autoselectKey) { ...@@ -375,8 +375,8 @@ function addAutoselectModel(autoselectKey) {
renderAutoselectModels(autoselectKey); renderAutoselectModels(autoselectKey);
} }
function removeAutoselectModel(autoselectKey, index) { async function removeAutoselectModel(autoselectKey, index) {
if (confirm('Remove this model?')) { if (await showDangerConfirm('Remove this model?', 'Remove Model')) {
autoselectConfig[autoselectKey].available_models.splice(index, 1); autoselectConfig[autoselectKey].available_models.splice(index, 1);
renderAutoselectModels(autoselectKey); renderAutoselectModels(autoselectKey);
} }
...@@ -399,10 +399,10 @@ async function saveAutoselect() { ...@@ -399,10 +399,10 @@ async function saveAutoselect() {
if (response.ok) { if (response.ok) {
window.location.href = autoselectData.successUrl; window.location.href = autoselectData.successUrl;
} else { } else {
alert('Error saving configuration'); showAlert('Error saving configuration', 'Error', '❌', 'danger');
} }
} catch (error) { } catch (error) {
alert('Error: ' + error.message); showAlert('Error: ' + error.message, 'Error', '❌', 'danger');
} }
} }
......
...@@ -8,7 +8,7 @@ ...@@ -8,7 +8,7 @@
<div style="display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: 15px;"> <div style="display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: 15px;">
<div> <div>
<div style="color: #a0a0a0; font-size: 14px; margin-bottom: 5px;">Wallet Balance</div> <div style="color: #a0a0a0; font-size: 14px; margin-bottom: 5px;">Wallet Balance</div>
<div style="font-size: 32px; font-weight: bold; color: #4ade80;">${{ wallet.balance }}</div> <div style="font-size: 32px; font-weight: bold; color: #4ade80;">{{ currency_symbol }}{{ wallet.balance }}</div>
<div style="color: #a0a0a0; font-size: 13px; margin-top: 5px;"> <div style="color: #a0a0a0; font-size: 13px; margin-top: 5px;">
All subscription renewals and payments are automatically charged from your wallet first. All subscription renewals and payments are automatically charged from your wallet first.
</div> </div>
...@@ -315,11 +315,12 @@ document.addEventListener('DOMContentLoaded', function() { ...@@ -315,11 +315,12 @@ document.addEventListener('DOMContentLoaded', function() {
if (!methodId) { if (!methodId) {
console.error('No method ID found'); console.error('No method ID found');
alert('Error: Payment method ID not found'); showAlert('Error: Payment method ID not found', 'Error', '❌', 'danger');
return; return;
} }
if (confirm(`Are you sure you want to delete this ${methodType || 'payment'} method?`)) { showDangerConfirm(`Are you sure you want to delete this ${methodType || 'payment'} method?`, 'Delete Payment Method').then(ok => {
if (!ok) return;
console.log('Sending DELETE request to:', `/dashboard/billing/payment-methods/${methodId}`); console.log('Sending DELETE request to:', `/dashboard/billing/payment-methods/${methodId}`);
fetch(`/dashboard/billing/payment-methods/${methodId}`, { fetch(`/dashboard/billing/payment-methods/${methodId}`, {
...@@ -335,17 +336,16 @@ document.addEventListener('DOMContentLoaded', function() { ...@@ -335,17 +336,16 @@ document.addEventListener('DOMContentLoaded', function() {
.then(data => { .then(data => {
console.log('Response data:', data); console.log('Response data:', data);
if (data.success) { if (data.success) {
alert('Payment method deleted successfully'); showAlert('Payment method deleted successfully', 'Done', '✅', 'info').then(() => window.location.reload());
window.location.reload();
} else { } else {
alert('Error: ' + (data.error || 'Failed to delete payment method')); showAlert('Error: ' + (data.error || 'Failed to delete payment method'), 'Error', '❌', 'danger');
} }
}) })
.catch(error => { .catch(error => {
console.error('Fetch error:', error); console.error('Fetch error:', error);
alert('An error occurred while deleting the payment method: ' + error.message); showAlert('An error occurred while deleting the payment method: ' + error.message, 'Error', '❌', 'danger');
}); });
} });
}); });
}); });
...@@ -363,7 +363,7 @@ document.addEventListener('DOMContentLoaded', function() { ...@@ -363,7 +363,7 @@ document.addEventListener('DOMContentLoaded', function() {
if (!methodId) { if (!methodId) {
console.error('No method ID found'); console.error('No method ID found');
alert('Error: Payment method ID not found'); showAlert('Error: Payment method ID not found', 'Error', '❌', 'danger');
return; return;
} }
...@@ -382,15 +382,14 @@ document.addEventListener('DOMContentLoaded', function() { ...@@ -382,15 +382,14 @@ document.addEventListener('DOMContentLoaded', function() {
.then(data => { .then(data => {
console.log('Response data:', data); console.log('Response data:', data);
if (data.success) { if (data.success) {
alert('Payment method set as default'); showAlert('Payment method set as default', 'Done', '✅', 'info').then(() => window.location.reload());
window.location.reload();
} else { } else {
alert('Error: ' + (data.error || 'Failed to set default payment method')); showAlert('Error: ' + (data.error || 'Failed to set default payment method'), 'Error', '❌', 'danger');
} }
}) })
.catch(error => { .catch(error => {
console.error('Fetch error:', error); console.error('Fetch error:', error);
alert('An error occurred while setting default payment method: ' + error.message); showAlert('An error occurred while setting default payment method: ' + error.message, 'Error', '❌', 'danger');
}); });
}); });
}); });
......
...@@ -185,13 +185,13 @@ ...@@ -185,13 +185,13 @@
</style> </style>
<script> <script>
function confirmDeletion() { async function confirmDeletion() {
const confirmation = document.getElementById('confirmation').value; const confirmation = document.getElementById('confirmation').value;
if (confirmation !== 'DELETE') { if (confirmation !== 'DELETE') {
alert('Please type "DELETE" exactly to confirm account deletion.'); showAlert('Please type "DELETE" exactly to confirm account deletion.', 'Validation Error', '⚠️', 'warn');
return false; return false;
} }
return confirm('Are you absolutely sure? This action cannot be undone and all your data will be permanently deleted.'); return await showDangerConfirm('Are you absolutely sure? This action cannot be undone and all your data will be permanently deleted.', 'Delete Account');
} }
</script> </script>
{% endblock %} {% endblock %}
...@@ -19,55 +19,94 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. ...@@ -19,55 +19,94 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
{% block title %}Overview - AISBF Dashboard{% endblock %} {% block title %}Overview - AISBF Dashboard{% endblock %}
{% block content %} {% block content %}
<h2 style="margin-bottom: 30px;">Dashboard Overview</h2> <style>
.overview-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 16px; margin-bottom: 32px; }
.stat-card { background: #1a1a2e; border: 1px solid #0f3460; border-radius: 10px; padding: 20px; display: flex; align-items: center; gap: 16px; transition: border-color .2s; }
.stat-card:hover { border-color: #e94560; }
.stat-icon { font-size: 2em; width: 48px; height: 48px; border-radius: 8px; display: flex; align-items: center; justify-content: center; flex-shrink: 0; }
.stat-icon.blue { background: rgba(52,152,219,.15); }
.stat-icon.green { background: rgba(46,204,113,.15); }
.stat-icon.red { background: rgba(233,69,96,.15); }
.stat-icon.purple{ background: rgba(155,89,182,.15); }
.stat-value { font-size: 2em; font-weight: 700; line-height: 1; }
.stat-label { font-size: .85em; color: #a0a0a0; margin-top: 2px; }
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 20px; margin-bottom: 30px;"> .info-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 20px; margin-bottom: 32px; }
<div style="background: #3498db; color: white; padding: 20px; border-radius: 8px;"> .info-card { background: #1a1a2e; border: 1px solid #0f3460; border-radius: 10px; padding: 20px; }
<h3 style="font-size: 16px; margin-bottom: 10px;">Providers</h3> .info-card h4 { font-size: .8em; text-transform: uppercase; letter-spacing: .08em; color: #a0a0a0; margin-bottom: 14px; }
<p style="font-size: 32px; font-weight: bold;">{{ providers_count }}</p> .info-row { display: flex; justify-content: space-between; align-items: center; padding: 8px 0; border-bottom: 1px solid #0f3460; font-size: .9em; }
.info-row:last-child { border-bottom: none; }
.info-row .val { color: #e0e0e0; font-weight: 500; }
.badge { display: inline-block; padding: 2px 10px; border-radius: 20px; font-size: .8em; font-weight: 600; }
.badge-green { background: rgba(46,204,113,.15); color: #2ecc71; }
.badge-red { background: rgba(231,76,60,.15); color: #e74c3c; }
.badge-blue { background: rgba(52,152,219,.15); color: #3498db; }
.quick-actions { display: flex; flex-wrap: wrap; gap: 10px; }
</style>
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:28px;">
<div>
<h2 style="margin:0; font-size:1.6em;">Dashboard Overview</h2>
<p style="color:#a0a0a0; margin:4px 0 0; font-size:.9em;">AISBF v{{ __version__ }} — Admin</p>
</div>
<a href="{{ url_for(request, '/dashboard/settings') }}" class="btn btn-secondary" style="margin:0;"><i class="fas fa-cog"></i> Settings</a>
</div>
<!-- Stats row -->
<div class="overview-grid">
<div class="stat-card">
<div class="stat-icon blue"><i class="fas fa-plug" style="color:#3498db;"></i></div>
<div>
<div class="stat-value" style="color:#3498db;">{{ providers_count }}</div>
<div class="stat-label">Providers</div>
</div>
</div>
<div class="stat-card">
<div class="stat-icon green"><i class="fas fa-sync-alt" style="color:#2ecc71;"></i></div>
<div>
<div class="stat-value" style="color:#2ecc71;">{{ rotations_count }}</div>
<div class="stat-label">Rotations</div>
</div>
</div> </div>
<div style="background: #2ecc71; color: white; padding: 20px; border-radius: 8px;"> <div class="stat-card">
<h3 style="font-size: 16px; margin-bottom: 10px;">Rotations</h3> <div class="stat-icon red"><i class="fas fa-brain" style="color:#e94560;"></i></div>
<p style="font-size: 32px; font-weight: bold;">{{ rotations_count }}</p> <div>
<div class="stat-value" style="color:#e94560;">{{ autoselect_count }}</div>
<div class="stat-label">Autoselects</div>
</div>
</div> </div>
<div style="background: #e74c3c; color: white; padding: 20px; border-radius: 8px;"> <div class="stat-card">
<h3 style="font-size: 16px; margin-bottom: 10px;">Autoselect</h3> <div class="stat-icon purple"><i class="fas fa-users" style="color:#9b59b6;"></i></div>
<p style="font-size: 32px; font-weight: bold;">{{ autoselect_count }}</p> <div>
<div class="stat-value" style="color:#9b59b6;">{{ users_count if users_count is defined else '—' }}</div>
<div class="stat-label">Users</div>
</div>
</div> </div>
</div> </div>
<h3 style="margin-bottom: 15px;">Server Information</h3> <!-- Info cards -->
<table> <div class="info-grid">
<tr> <div class="info-card">
<th>Setting</th> <h4><i class="fas fa-server"></i> &nbsp;Server</h4>
<th>Value</th> <div class="info-row"><span>Host</span><span class="val">{{ server_config.host }}</span></div>
</tr> <div class="info-row"><span>Port</span><span class="val">{{ server_config.port }}</span></div>
<tr> <div class="info-row"><span>Protocol</span><span class="val"><span class="badge {% if server_config.protocol == 'https' %}badge-green{% else %}badge-blue{% endif %}">{{ server_config.protocol|upper }}</span></span></div>
<td>Host</td> <div class="info-row"><span>Auth</span><span class="val"><span class="badge {% if server_config.auth_enabled %}badge-green{% else %}badge-red{% endif %}">{{ 'Enabled' if server_config.auth_enabled else 'Disabled' }}</span></span></div>
<td>{{ server_config.host }}</td> </div>
</tr>
<tr>
<td>Port</td>
<td>{{ server_config.port }}</td>
</tr>
<tr>
<td>Protocol</td>
<td>{{ server_config.protocol }}</td>
</tr>
<tr>
<td>Authentication</td>
<td>{{ 'Enabled' if server_config.auth_enabled else 'Disabled' }}</td>
</tr>
</table>
<h3 style="margin-top: 30px; margin-bottom: 15px;">Quick Actions</h3> <div class="info-card">
<div style="display: flex; gap: 10px; flex-wrap: wrap;"> <h4><i class="fas fa-bolt"></i> &nbsp;Quick Actions</h4>
<a href="{{ url_for(request, '/dashboard/providers') }}" class="btn">Manage Providers</a> <div class="quick-actions" style="margin-top:4px;">
<a href="{{ url_for(request, '/dashboard/rotations') }}" class="btn">Manage Rotations</a> <a href="{{ url_for(request, '/dashboard/providers') }}" class="btn" style="margin:0; font-size:.85em; padding:8px 14px;"><i class="fas fa-plug"></i> Providers</a>
<a href="{{ url_for(request, '/dashboard/autoselect') }}" class="btn">Manage Autoselect</a> <a href="{{ url_for(request, '/dashboard/rotations') }}" class="btn btn-secondary" style="margin:0; font-size:.85em; padding:8px 14px;"><i class="fas fa-sync-alt"></i> Rotations</a>
<a href="{{ url_for(request, '/dashboard/prompts') }}" class="btn">Manage Prompts</a> <a href="{{ url_for(request, '/dashboard/autoselect') }}" class="btn btn-secondary" style="margin:0; font-size:.85em; padding:8px 14px;"><i class="fas fa-brain"></i> Autoselect</a>
<a href="{{ url_for(request, '/dashboard/rate-limits') }}" class="btn">Rate Limits</a> <a href="{{ url_for(request, '/dashboard/users') }}" class="btn btn-secondary" style="margin:0; font-size:.85em; padding:8px 14px;"><i class="fas fa-users"></i> Users</a>
<a href="{{ url_for(request, '/dashboard/response-cache') }}" class="btn">Response Cache</a> <a href="{{ url_for(request, '/dashboard/analytics') }}" class="btn btn-secondary" style="margin:0; font-size:.85em; padding:8px 14px;"><i class="fas fa-chart-bar"></i> Analytics</a>
<a href="{{ url_for(request, '/dashboard/settings') }}" class="btn btn-secondary">Server Settings</a> <a href="{{ url_for(request, '/dashboard/rate-limits') }}" class="btn btn-secondary" style="margin:0; font-size:.85em; padding:8px 14px;"><i class="fas fa-tachometer-alt"></i> Rate Limits</a>
<a href="{{ url_for(request, '/dashboard/response-cache') }}" class="btn btn-secondary" style="margin:0; font-size:.85em; padding:8px 14px;"><i class="fas fa-database"></i> Cache</a>
<a href="{{ url_for(request, '/dashboard/prompts') }}" class="btn btn-secondary" style="margin:0; font-size:.85em; padding:8px 14px;"><i class="fas fa-comment-dots"></i> Prompts</a>
</div>
</div>
</div> </div>
{% endblock %} {% endblock %}
...@@ -143,7 +143,7 @@ function openOAuthPopup(url) { ...@@ -143,7 +143,7 @@ function openOAuthPopup(url) {
window.removeEventListener('message', messageListener); window.removeEventListener('message', messageListener);
// Show error // Show error
alert(event.data.error || 'Authentication failed'); showAlert(event.data.error || 'Authentication failed', 'Authentication Error', '❌', 'danger');
} }
}; };
......
...@@ -216,8 +216,9 @@ ...@@ -216,8 +216,9 @@
{% block extra_js %} {% block extra_js %}
<script> <script>
function subscribeToTier(tierId) { async function subscribeToTier(tierId) {
if (confirm('Are you sure you want to upgrade to this tier?')) { const ok = await showConfirm('Are you sure you want to upgrade to this tier?', 'Upgrade Plan');
if (ok) {
fetch(url_for(`/api/subscribe/${tierId}`), { fetch(url_for(`/api/subscribe/${tierId}`), {
method: 'POST', method: 'POST',
headers: { headers: {
...@@ -241,8 +242,9 @@ function subscribeToTier(tierId) { ...@@ -241,8 +242,9 @@ function subscribeToTier(tierId) {
} }
} }
function downgradeToFree() { async function downgradeToFree() {
if (confirm('Are you sure you want to downgrade to the free tier? Your current subscription will remain active until the end of the billing period.')) { const ok = await showConfirm('Are you sure you want to downgrade to the free tier? Your current subscription will remain active until the end of the billing period.', 'Downgrade Plan');
if (ok) {
fetch(url_for('/api/subscribe/free'), { fetch(url_for('/api/subscribe/free'), {
method: 'POST', method: 'POST',
headers: { headers: {
......
...@@ -72,9 +72,10 @@ function switchPrompt() { ...@@ -72,9 +72,10 @@ function switchPrompt() {
} }
} }
function resetPrompt() { async function resetPrompt() {
if (!currentPromptKey) return; if (!currentPromptKey) return;
if (confirm('Are you sure you want to reset this prompt to the default admin configuration?')) { const ok = await showDangerConfirm('Are you sure you want to reset this prompt to the default admin configuration?', 'Reset Prompt');
if (ok) {
fetch('/dashboard/prompts/reset/' + currentPromptKey, { fetch('/dashboard/prompts/reset/' + currentPromptKey, {
method: 'POST', method: 'POST',
headers: { headers: {
......
...@@ -134,7 +134,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. ...@@ -134,7 +134,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
{% block extra_js %} {% block extra_js %}
<script> <script>
let providersData = {{ providers_json | safe }}; let providersData = {{ providers_json | replace("</script>", "<\\/script>") | safe }};
let expandedProviders = new Set(); let expandedProviders = new Set();
// Chunk size: 512KB chunks for maximum compatibility with restrictive proxies // Chunk size: 512KB chunks for maximum compatibility with restrictive proxies
...@@ -1603,8 +1603,8 @@ async function pollKiloAuth(providerKey, deviceCode) { ...@@ -1603,8 +1603,8 @@ async function pollKiloAuth(providerKey, deviceCode) {
// This function is no longer needed as polling is handled in authenticateKilo // This function is no longer needed as polling is handled in authenticateKilo
} }
function removeProvider(key) { async function removeProvider(key) {
if (confirm(`Remove provider "${key}"?`)) { if (await showDangerConfirm(`Remove provider "${key}"?`, 'Remove Provider')) {
delete providersData[key]; delete providersData[key];
expandedProviders.delete(key); expandedProviders.delete(key);
renderProvidersList(); renderProvidersList();
...@@ -1684,8 +1684,8 @@ function addModel(key) { ...@@ -1684,8 +1684,8 @@ function addModel(key) {
renderModels(key); renderModels(key);
} }
function removeModel(providerKey, index) { async function removeModel(providerKey, index) {
if (confirm('Remove this model?')) { if (await showDangerConfirm('Remove this model?', 'Remove Model')) {
providersData[providerKey].models.splice(index, 1); providersData[providerKey].models.splice(index, 1);
renderModels(providerKey); renderModels(providerKey);
} }
...@@ -1696,12 +1696,12 @@ async function confirmAddProvider() { ...@@ -1696,12 +1696,12 @@ async function confirmAddProvider() {
const type = document.getElementById('new-provider-type').value; const type = document.getElementById('new-provider-type').value;
if (!key) { if (!key) {
alert('Please enter a provider key'); showAlert('Please enter a provider key', 'Missing Key', '⚠️', 'warn');
return; return;
} }
if (providersData[key]) { if (providersData[key]) {
alert('Provider key already exists'); showAlert('Provider key already exists', 'Duplicate Key', '⚠️', 'warn');
return; return;
} }
...@@ -1836,10 +1836,10 @@ async function saveProviders() { ...@@ -1836,10 +1836,10 @@ async function saveProviders() {
if (response.ok) { if (response.ok) {
window.location.href = '/dashboard/providers?success=1'; window.location.href = '/dashboard/providers?success=1';
} else { } else {
alert('Error saving configuration'); showAlert('Error saving configuration', 'Error', '❌', 'danger');
} }
} catch (error) { } catch (error) {
alert('Error: ' + error.message); showAlert('Error: ' + error.message, 'Error', '❌', 'danger');
} }
} }
......
...@@ -169,9 +169,8 @@ async function loadRateLimits() { ...@@ -169,9 +169,8 @@ async function loadRateLimits() {
} }
async function resetRateLimiter(providerId) { async function resetRateLimiter(providerId) {
if (!confirm(`Reset rate limiter for ${providerId}?`)) { const ok = await showConfirm(`Reset rate limiter for ${providerId}?`, 'Reset Rate Limiter');
return; if (!ok) return;
}
try { try {
const response = await fetch(`/dashboard/rate-limits/${providerId}/reset`, { const response = await fetch(`/dashboard/rate-limits/${providerId}/reset`, {
...@@ -180,20 +179,19 @@ async function resetRateLimiter(providerId) { ...@@ -180,20 +179,19 @@ async function resetRateLimiter(providerId) {
const data = await response.json(); const data = await response.json();
if (data.success) { if (data.success) {
alert(data.message); showAlert(data.message, 'Done', '✅', 'info');
loadRateLimits(); loadRateLimits();
} else { } else {
alert('Error: ' + data.error); showAlert('Error: ' + data.error, 'Error', '❌', 'danger');
} }
} catch (error) { } catch (error) {
alert('Error: ' + error.message); showAlert('Error: ' + error.message, 'Error', '❌', 'danger');
} }
} }
async function clearAllRateLimiters() { async function clearAllRateLimiters() {
if (!confirm('Reset all rate limiters? This will clear all learned rate limits.')) { const ok = await showDangerConfirm('Reset all rate limiters? This will clear all learned rate limits.', 'Reset All');
return; if (!ok) return;
}
const content = document.getElementById('rate-limits-content'); const content = document.getElementById('rate-limits-content');
const providerIds = []; const providerIds = [];
...@@ -213,10 +211,10 @@ async function clearAllRateLimiters() { ...@@ -213,10 +211,10 @@ async function clearAllRateLimiters() {
} }
} }
alert('All rate limiters reset successfully'); showAlert('All rate limiters reset successfully', 'Done', '✅', 'info');
loadRateLimits(); loadRateLimits();
} catch (error) { } catch (error) {
alert('Error: ' + error.message); showAlert('Error: ' + error.message, 'Error', '❌', 'danger');
} }
} }
......
...@@ -122,29 +122,24 @@ function refreshStats() { ...@@ -122,29 +122,24 @@ function refreshStats() {
location.reload(); location.reload();
} }
function clearCache() { async function clearCache() {
if (!confirm('Are you sure you want to clear the entire response cache? This action cannot be undone.')) { const ok = await showDangerConfirm('Are you sure you want to clear the entire response cache? This action cannot be undone.', 'Clear Cache');
return; if (!ok) return;
}
fetch('{{ url_for(request, "/dashboard/response-cache/clear") }}', { try {
method: 'POST', const response = await fetch('{{ url_for(request, "/dashboard/response-cache/clear") }}', {
headers: { method: 'POST',
'Content-Type': 'application/json' headers: {'Content-Type': 'application/json'}
} });
}) const data = await response.json();
.then(response => response.json())
.then(data => {
if (data.success) { if (data.success) {
alert('Cache cleared successfully!'); showAlert('Cache cleared successfully!', 'Done', '✅', 'info').then(() => location.reload());
location.reload();
} else { } else {
alert('Error clearing cache: ' + (data.error || 'Unknown error')); showAlert('Error clearing cache: ' + (data.error || 'Unknown error'), 'Error', '❌', 'danger');
} }
}) } catch (error) {
.catch(error => { showAlert('Error clearing cache: ' + error, 'Error', '❌', 'danger');
alert('Error clearing cache: ' + error); }
});
} }
</script> </script>
......
...@@ -268,10 +268,11 @@ function updateGlobalNotify(value) { ...@@ -268,10 +268,11 @@ function updateGlobalNotify(value) {
rotationsConfig.notifyerrors = value; rotationsConfig.notifyerrors = value;
} }
function addRotation() { async function addRotation() {
const key = prompt('Enter rotation key (e.g., "coding", "general"):'); const key = await showPrompt('Enter rotation key (e.g., "coding", "general"):', '', 'rotation-key', 'Add Rotation');
if (!key || rotationsConfig.rotations[key]) { if (!key) return;
alert('Invalid or duplicate rotation key'); if (rotationsConfig.rotations[key]) {
showAlert('Rotation key already exists', 'Duplicate Key', '⚠️', 'warn');
return; return;
} }
...@@ -290,8 +291,8 @@ function addRotation() { ...@@ -290,8 +291,8 @@ function addRotation() {
renderRotationsList(); renderRotationsList();
} }
function removeRotation(key) { async function removeRotation(key) {
if (confirm(`Remove rotation "${key}"?`)) { if (await showDangerConfirm(`Remove rotation "${key}"?`, 'Remove Rotation')) {
delete rotationsConfig.rotations[key]; delete rotationsConfig.rotations[key];
expandedRotations.delete(key); expandedRotations.delete(key);
renderRotationsList(); renderRotationsList();
...@@ -327,8 +328,8 @@ function addRotationProvider(rotationKey) { ...@@ -327,8 +328,8 @@ function addRotationProvider(rotationKey) {
renderRotationProviders(rotationKey); renderRotationProviders(rotationKey);
} }
function removeRotationProvider(rotationKey, providerIndex) { async function removeRotationProvider(rotationKey, providerIndex) {
if (confirm('Remove this provider?')) { if (await showDangerConfirm('Remove this provider?', 'Remove Provider')) {
rotationsConfig.rotations[rotationKey].providers.splice(providerIndex, 1); rotationsConfig.rotations[rotationKey].providers.splice(providerIndex, 1);
renderRotationProviders(rotationKey); renderRotationProviders(rotationKey);
} }
...@@ -353,8 +354,8 @@ function addRotationModel(rotationKey, providerIndex) { ...@@ -353,8 +354,8 @@ function addRotationModel(rotationKey, providerIndex) {
renderRotationModels(rotationKey, providerIndex); renderRotationModels(rotationKey, providerIndex);
} }
function removeRotationModel(rotationKey, providerIndex, modelIndex) { async function removeRotationModel(rotationKey, providerIndex, modelIndex) {
if (confirm('Remove this model?')) { if (await showDangerConfirm('Remove this model?', 'Remove Model')) {
rotationsConfig.rotations[rotationKey].providers[providerIndex].models.splice(modelIndex, 1); rotationsConfig.rotations[rotationKey].providers[providerIndex].models.splice(modelIndex, 1);
renderRotationModels(rotationKey, providerIndex); renderRotationModels(rotationKey, providerIndex);
} }
...@@ -393,10 +394,10 @@ async function saveRotations() { ...@@ -393,10 +394,10 @@ async function saveRotations() {
if (response.ok) { if (response.ok) {
window.location.href = rotationsData.successUrl; window.location.href = rotationsData.successUrl;
} else { } else {
alert('Error saving configuration'); showAlert('Error saving configuration', 'Error', '❌', 'danger');
} }
} catch (error) { } catch (error) {
alert('Error: ' + error.message); showAlert('Error: ' + error.message, 'Error', '❌', 'danger');
} }
} }
......
This diff is collapsed.
...@@ -117,36 +117,13 @@ document.querySelector('form')?.addEventListener('submit', function(e) { ...@@ -117,36 +117,13 @@ document.querySelector('form')?.addEventListener('submit', function(e) {
const password = document.getElementById('password').value; const password = document.getElementById('password').value;
const confirmPassword = document.getElementById('confirm_password').value; const confirmPassword = document.getElementById('confirm_password').value;
if (password !== confirmPassword) { const check = (msg) => { e.preventDefault(); showAlert(msg, 'Validation Error', '⚠️', 'warn'); return false; };
e.preventDefault();
alert('Passwords do not match!');
return false;
}
// Check password strength if (password !== confirmPassword) return check('Passwords do not match!');
if (password.length < 8) { if (password.length < 8) return check('Password must be at least 8 characters long!');
e.preventDefault(); if (!/[a-z]/.test(password)) return check('Password must contain at least one lowercase letter!');
alert('Password must be at least 8 characters long!'); if (!/[A-Z]/.test(password)) return check('Password must contain at least one uppercase letter!');
return false; if (!/\d/.test(password)) return check('Password must contain at least one number!');
}
if (!/[a-z]/.test(password)) {
e.preventDefault();
alert('Password must contain at least one lowercase letter!');
return false;
}
if (!/[A-Z]/.test(password)) {
e.preventDefault();
alert('Password must contain at least one uppercase letter!');
return false;
}
if (!/\d/.test(password)) {
e.preventDefault();
alert('Password must contain at least one number!');
return false;
}
return true; return true;
}); });
...@@ -189,7 +166,7 @@ function openOAuthPopup(url) { ...@@ -189,7 +166,7 @@ function openOAuthPopup(url) {
window.removeEventListener('message', messageListener); window.removeEventListener('message', messageListener);
// Show error // Show error
alert(event.data.error || 'Authentication failed'); showAlert(event.data.error || 'Authentication failed', 'Authentication Error', '❌', 'danger');
} }
}; };
......
...@@ -315,14 +315,14 @@ function renderAutoselectModels(autoselectKey) { ...@@ -315,14 +315,14 @@ function renderAutoselectModels(autoselectKey) {
}); });
} }
function addAutoselect() { async function addAutoselect() {
const key = prompt('Enter autoselect key (e.g., "autoselect", "smart-select"):'); const key = await showPrompt('Enter autoselect key (e.g., "autoselect", "smart-select"):', '', 'e.g. autoselect', 'Add Autoselect');
if (!key) { if (!key) {
return; return;
} }
if (autoselectConfig[key]) { if (autoselectConfig[key]) {
alert('Autoselect key already exists'); showAlert('Autoselect key already exists', 'Duplicate Key', '⚠️', 'warn');
return; return;
} }
...@@ -338,8 +338,8 @@ function addAutoselect() { ...@@ -338,8 +338,8 @@ function addAutoselect() {
renderAutoselectList(); renderAutoselectList();
} }
function removeAutoselect(key) { async function removeAutoselect(key) {
if (confirm(`Remove autoselect "${key}"?`)) { if (await showDangerConfirm(`Remove autoselect "${key}"?`, 'Remove Autoselect')) {
delete autoselectConfig[key]; delete autoselectConfig[key];
expandedAutoselects.delete(key); expandedAutoselects.delete(key);
renderAutoselectList(); renderAutoselectList();
...@@ -375,8 +375,8 @@ function addAutoselectModel(autoselectKey) { ...@@ -375,8 +375,8 @@ function addAutoselectModel(autoselectKey) {
renderAutoselectModels(autoselectKey); renderAutoselectModels(autoselectKey);
} }
function removeAutoselectModel(autoselectKey, index) { async function removeAutoselectModel(autoselectKey, index) {
if (confirm('Remove this model?')) { if (await showDangerConfirm('Remove this model?', 'Remove Model')) {
autoselectConfig[autoselectKey].available_models.splice(index, 1); autoselectConfig[autoselectKey].available_models.splice(index, 1);
renderAutoselectModels(autoselectKey); renderAutoselectModels(autoselectKey);
} }
...@@ -399,10 +399,10 @@ async function saveAutoselect() { ...@@ -399,10 +399,10 @@ async function saveAutoselect() {
if (response.ok) { if (response.ok) {
window.location.href = autoselectData.successUrl; window.location.href = autoselectData.successUrl;
} else { } else {
alert('Error saving configuration'); showAlert('Error saving configuration', 'Error', '❌', 'danger');
} }
} catch (error) { } catch (error) {
alert('Error: ' + error.message); showAlert('Error: ' + error.message, 'Error', '❌', 'danger');
} }
} }
......
This diff is collapsed.
...@@ -256,7 +256,7 @@ function showToast(message, type) { ...@@ -256,7 +256,7 @@ function showToast(message, type) {
let providersData = {}; let providersData = {};
let expandedProviders = new Set(); let expandedProviders = new Set();
let rawProviders = {{ user_providers_json | safe }}; let rawProviders = {{ user_providers_json | replace("</script>", "<\\/script>") | safe }};
// Convert user providers format to the format expected by the UI // Convert user providers format to the format expected by the UI
rawProviders.forEach(provider => { rawProviders.forEach(provider => {
...@@ -1705,8 +1705,8 @@ async function pollKiloAuth(providerKey, deviceCode) { ...@@ -1705,8 +1705,8 @@ async function pollKiloAuth(providerKey, deviceCode) {
// This function is no longer needed as polling is handled in authenticateKilo // This function is no longer needed as polling is handled in authenticateKilo
} }
function removeProvider(key) { async function removeProvider(key) {
if (confirm(`Remove provider "${key}"?`)) { if (await showDangerConfirm(`Remove provider "${key}"?`, 'Remove Provider')) {
delete providersData[key]; delete providersData[key];
expandedProviders.delete(key); expandedProviders.delete(key);
renderProvidersList(); renderProvidersList();
...@@ -1786,8 +1786,8 @@ function addModel(key) { ...@@ -1786,8 +1786,8 @@ function addModel(key) {
renderModels(key); renderModels(key);
} }
function removeModel(providerKey, index) { async function removeModel(providerKey, index) {
if (confirm('Remove this model?')) { if (await showDangerConfirm('Remove this model?', 'Remove Model')) {
providersData[providerKey].models.splice(index, 1); providersData[providerKey].models.splice(index, 1);
renderModels(providerKey); renderModels(providerKey);
} }
...@@ -1798,12 +1798,12 @@ async function confirmAddProvider() { ...@@ -1798,12 +1798,12 @@ async function confirmAddProvider() {
const type = document.getElementById('new-provider-type').value; const type = document.getElementById('new-provider-type').value;
if (!key) { if (!key) {
alert('Please enter a provider key'); showAlert('Please enter a provider key', 'Missing Key', '⚠️', 'warn');
return; return;
} }
if (providersData[key]) { if (providersData[key]) {
alert('Provider key already exists'); showAlert('Provider key already exists', 'Duplicate Key', '⚠️', 'warn');
return; return;
} }
...@@ -1931,10 +1931,10 @@ async function saveProviders() { ...@@ -1931,10 +1931,10 @@ async function saveProviders() {
if (response.ok) { if (response.ok) {
window.location.href = '/dashboard/providers?success=1'; window.location.href = '/dashboard/providers?success=1';
} else { } else {
alert('Error saving configuration'); showAlert('Error saving configuration', 'Error', '❌', 'danger');
} }
} catch (error) { } catch (error) {
alert('Error: ' + error.message); showAlert('Error: ' + error.message, 'Error', '❌', 'danger');
} }
} }
......
...@@ -259,10 +259,11 @@ function renderRotationModels(rotationKey, providerIndex) { ...@@ -259,10 +259,11 @@ function renderRotationModels(rotationKey, providerIndex) {
}); });
} }
function addRotation() { async function addRotation() {
const key = prompt('Enter rotation key (e.g., "coding", "general"):'); const key = await showPrompt('Enter rotation key (e.g., "coding", "general"):', '', 'rotation-key', 'Add Rotation');
if (!key || rotationsConfig.rotations[key]) { if (!key) return;
alert('Invalid or duplicate rotation key'); if (rotationsConfig.rotations[key]) {
showAlert('Rotation key already exists', 'Duplicate Key', '⚠️', 'warn');
return; return;
} }
...@@ -281,8 +282,8 @@ function addRotation() { ...@@ -281,8 +282,8 @@ function addRotation() {
renderRotationsList(); renderRotationsList();
} }
function removeRotation(key) { async function removeRotation(key) {
if (confirm(`Remove rotation "${key}"?`)) { if (await showDangerConfirm(`Remove rotation "${key}"?`, 'Remove Rotation')) {
delete rotationsConfig.rotations[key]; delete rotationsConfig.rotations[key];
expandedRotations.delete(key); expandedRotations.delete(key);
renderRotationsList(); renderRotationsList();
...@@ -318,8 +319,8 @@ function addRotationProvider(rotationKey) { ...@@ -318,8 +319,8 @@ function addRotationProvider(rotationKey) {
renderRotationProviders(rotationKey); renderRotationProviders(rotationKey);
} }
function removeRotationProvider(rotationKey, providerIndex) { async function removeRotationProvider(rotationKey, providerIndex) {
if (confirm('Remove this provider?')) { if (await showDangerConfirm('Remove this provider?', 'Remove Provider')) {
rotationsConfig.rotations[rotationKey].providers.splice(providerIndex, 1); rotationsConfig.rotations[rotationKey].providers.splice(providerIndex, 1);
renderRotationProviders(rotationKey); renderRotationProviders(rotationKey);
} }
...@@ -344,8 +345,8 @@ function addRotationModel(rotationKey, providerIndex) { ...@@ -344,8 +345,8 @@ function addRotationModel(rotationKey, providerIndex) {
renderRotationModels(rotationKey, providerIndex); renderRotationModels(rotationKey, providerIndex);
} }
function removeRotationModel(rotationKey, providerIndex, modelIndex) { async function removeRotationModel(rotationKey, providerIndex, modelIndex) {
if (confirm('Remove this model?')) { if (await showDangerConfirm('Remove this model?', 'Remove Model')) {
rotationsConfig.rotations[rotationKey].providers[providerIndex].models.splice(modelIndex, 1); rotationsConfig.rotations[rotationKey].providers[providerIndex].models.splice(modelIndex, 1);
renderRotationModels(rotationKey, providerIndex); renderRotationModels(rotationKey, providerIndex);
} }
...@@ -384,10 +385,10 @@ async function saveRotations() { ...@@ -384,10 +385,10 @@ async function saveRotations() {
if (response.ok) { if (response.ok) {
window.location.href = rotationsData.successUrl; window.location.href = rotationsData.successUrl;
} else { } else {
alert('Error saving configuration'); showAlert('Error saving configuration', 'Error', '❌', 'danger');
} }
} catch (error) { } catch (error) {
alert('Error: ' + error.message); showAlert('Error: ' + error.message, 'Error', '❌', 'danger');
} }
} }
......
This diff is collapsed.
...@@ -688,7 +688,7 @@ function clearSelection() { ...@@ -688,7 +688,7 @@ function clearSelection() {
updateBulkActionsVisibility(); updateBulkActionsVisibility();
} }
function performBulkAction(action, description, destructive = false, extraData = null) { async function performBulkAction(action, description, destructive = false, extraData = null) {
const selectedIds = Array.from(document.querySelectorAll('.user-checkbox:checked')).map(cb => parseInt(cb.value)); const selectedIds = Array.from(document.querySelectorAll('.user-checkbox:checked')).map(cb => parseInt(cb.value));
if (selectedIds.length === 0) { if (selectedIds.length === 0) {
...@@ -696,8 +696,9 @@ function performBulkAction(action, description, destructive = false, extraData = ...@@ -696,8 +696,9 @@ function performBulkAction(action, description, destructive = false, extraData =
return; return;
} }
if (destructive && !confirm(`Are you sure you want to ${description}? This action cannot be undone.`)) { if (destructive) {
return; const ok = await showDangerConfirm(`Are you sure you want to ${description}? This action cannot be undone.`, 'Confirm Action');
if (!ok) return;
} }
// Show loading state // Show loading state
...@@ -754,9 +755,10 @@ function closeEditModal() { ...@@ -754,9 +755,10 @@ function closeEditModal() {
document.getElementById('edit-modal').style.display = 'none'; document.getElementById('edit-modal').style.display = 'none';
} }
function toggleUserStatus(userId, currentStatus) { async function toggleUserStatus(userId, currentStatus) {
const action = currentStatus ? 'disable' : 'enable'; const action = currentStatus ? 'disable' : 'enable';
if (confirm('Are you sure you want to ' + action + ' this user?')) { const ok = await showConfirm('Are you sure you want to ' + action + ' this user?', 'Confirm');
if (ok) {
fetch(baseUrl + userId + '/toggle', { fetch(baseUrl + userId + '/toggle', {
method: 'POST', method: 'POST',
headers: {'Content-Type': 'application/json'} headers: {'Content-Type': 'application/json'}
...@@ -766,17 +768,18 @@ function toggleUserStatus(userId, currentStatus) { ...@@ -766,17 +768,18 @@ function toggleUserStatus(userId, currentStatus) {
if (data.success) { if (data.success) {
location.reload(); location.reload();
} else { } else {
alert(data.error || 'Failed to toggle user status'); showAlert(data.error || 'Failed to toggle user status', 'Error', '❌', 'danger');
} }
}) })
.catch(error => { .catch(error => {
alert('Error: ' + error); showAlert('Error: ' + error, 'Error', '❌', 'danger');
}); });
} }
} }
function deleteUser(userId, username) { async function deleteUser(userId, username) {
if (confirm('Are you sure you want to delete user "' + username + '"? This action cannot be undone.')) { const ok = await showDangerConfirm('Are you sure you want to delete user "' + username + '"? This action cannot be undone.', 'Delete User');
if (ok) {
fetch(baseUrl + userId + '/delete', { fetch(baseUrl + userId + '/delete', {
method: 'POST', method: 'POST',
headers: {'Content-Type': 'application/json'} headers: {'Content-Type': 'application/json'}
...@@ -786,11 +789,11 @@ function deleteUser(userId, username) { ...@@ -786,11 +789,11 @@ function deleteUser(userId, username) {
if (data.success) { if (data.success) {
location.reload(); location.reload();
} else { } else {
alert(data.error || 'Failed to delete user'); showAlert(data.error || 'Failed to delete user', 'Error', '❌', 'danger');
} }
}) })
.catch(error => { .catch(error => {
alert('Error: ' + error); showAlert('Error: ' + error, 'Error', '❌', 'danger');
}); });
} }
} }
......
This diff is collapsed.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment