Revamp the dashboard interface

parent 59a08356
......@@ -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.
## 🚀 Support AISBF - Your Donations Matter!
---
The project includes multiple donation options to support its development:
## 🌐 Try AISBF Now — No Installation Required!
### Ethereum Donation
ETH to `0xdA6dAb526515b5cb556d20269207D43fcc760E51`
> **[➡️ Launch AISBF at https://aisbf.cloud](https://aisbf.cloud)**
>
> 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
https://paypal.me/nexlab
### 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!
Also available via TOR for privacy-first access:
[http://aisbfity4ud6nsht53tsh2iauaur2e4dah2gplcprnikyjpkg72vfjad.onion](http://aisbfity4ud6nsht53tsh2iauaur2e4dah2gplcprnikyjpkg72vfjad.onion)
![AISBF Dashboard](https://git.nexlab.net/nexlab/aisbf/raw/master/screenshot.png)
---
## Key Features
- **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:
- **[🔌 API Reference](DOCUMENTATION.md#api-endpoints)** - Complete API documentation
- **[🛠️ 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
Stefy Lanza <stefy@nexlab.net>
......
......@@ -2291,7 +2291,11 @@ async def dashboard_analytics(
user_filter_int = current_user_id
# 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
available_providers = list(config.providers.keys()) if config else []
......@@ -2379,7 +2383,8 @@ async def dashboard_analytics(
"selected_rotation": rotation_filter,
"selected_autoselect": autoselect_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:
logger.info(f"User authenticated: username={username}, email={user.get('email')}, user_id={user['id']}")
request.session['logged_in'] = True
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['role'] = user['role']
request.session['user_id'] = user['id']
......@@ -3203,8 +3209,9 @@ async def dashboard_profile_save(request: Request, username: str = Form(...), di
try:
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['display_name'] = display_name or ''
return RedirectResponse(url=url_for(request, "/dashboard/profile?success=Profile updated successfully"), status_code=303)
except Exception as e:
......@@ -4031,7 +4038,8 @@ async def dashboard_index(request: Request):
"subscription": subscription,
"current_tier": current_tier,
"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):
name="dashboard/admin_payment_settings.html",
context={
"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):
all_gateways = db.get_payment_gateway_settings()
enabled_gateways = {k: v for k, v in all_gateways.items() if v.get('enabled', False)}
return templates.TemplateResponse("dashboard/wallet.html", {
"request": request,
"wallet": wallet,
"enabled_gateways": enabled_gateways,
})
return templates.TemplateResponse(
request=request,
name="dashboard/wallet.html",
context={
"request": request,
"wallet": wallet,
"enabled_gateways": enabled_gateways,
"currency_symbol": db.get_currency_settings().get('currency_symbol', '$'),
}
)
except ImportError:
return HTMLResponse("Wallet functionality not available", status_code=503)
except Exception as e:
......@@ -8329,7 +8343,8 @@ async def dashboard_billing(request: Request):
"payment_methods": payment_methods,
"transactions": transactions,
"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() {
if (data.success) {
window.location.href = '/dashboard/billing?success=' + encodeURIComponent(data.message);
} else {
alert('Error: ' + data.error);
showAlert('Error: ' + data.error, 'Error', '❌', 'danger');
}
});
});
......@@ -165,7 +165,7 @@ document.addEventListener('DOMContentLoaded', function() {
const stripeKey = '{{ stripe_publishable_key }}';
if (!stripeKey) {
alert('Stripe is not configured. Please contact the administrator.');
showAlert('Stripe is not configured. Please contact the administrator.', 'Not Configured', '⚠️', 'warn');
return;
}
......
......@@ -19,6 +19,49 @@
</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 -->
<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;">
......@@ -638,9 +681,8 @@ async function saveEncryptionKey() {
return;
}
if (!confirm('WARNING: Setting or changing the encryption key will affect all encrypted data. Are you sure?')) {
return;
}
const ok = await showDangerConfirm('WARNING: Setting or changing the encryption key will affect all encrypted data. Are you sure?', 'Change Encryption Key');
if (!ok) return;
try {
const response = await fetch('{{ url_for(request, "/api/admin/settings/encryption-key") }}', {
......@@ -678,7 +720,7 @@ async function loadSystemStatus() {
}
// 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('failedPayments').textContent = data.failed_payments || 0;
} catch (error) {
......@@ -1062,6 +1104,7 @@ document.addEventListener('DOMContentLoaded', function() {
loadConfiguration();
loadPaymentGateways();
loadCryptoPrices();
loadCurrencySettings();
// Auto-refresh crypto prices every 60 seconds
setInterval(loadCryptoPrices, 60000);
......@@ -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
function refreshCryptoPrices() {
['Btc', 'Eth', 'Usdt', 'Usdc'].forEach(crypto => {
......
......@@ -69,49 +69,6 @@
</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 -->
<div class="modal fade" id="tierModal" tabindex="-1" aria-labelledby="tierModalLabel" aria-hidden="true" style="display: none;">
......@@ -229,7 +186,6 @@
<script>
document.addEventListener('DOMContentLoaded', function() {
try {
loadCurrencySettings();
} catch (error) {
console.error('Error initializing page:', error);
}
......@@ -403,15 +359,15 @@ function closeTierModal() {
document.getElementById('tierModal').style.display = 'none';
}
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.')) {
async function deleteTier(tierId) {
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, {
method: 'DELETE'
})
.then(response => response.json())
.then(result => {
showToast('Tier deleted successfully', 'success');
// Reload page to reflect changes
window.location.reload();
})
.catch(error => {
......@@ -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) {
......
......@@ -99,7 +99,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
</span>
{% if date_range_usage %}
<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>
{% endif %}
</div>
......@@ -494,7 +494,7 @@ document.getElementById('timeRangeSelect').addEventListener('change', function()
{% if total_requests > 0 %}
{% set weighted_sum = namespace(value=0) %}
{% 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 %}
{% 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 %}
......@@ -521,7 +521,7 @@ document.getElementById('timeRangeSelect').addEventListener('change', function()
<h4 style="font-size: 14px; margin-bottom: 10px;">
{% if from_date or to_date %}Selected Period Cost{% else %}Today's Estimated Cost{% endif %}
</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 %}
<small style="color: rgba(255,255,255,0.8);">
{{ cost_overview.date_range.start[:10] }} to {{ cost_overview.date_range.end[:10] }}
......
......@@ -315,14 +315,14 @@ function renderAutoselectModels(autoselectKey) {
});
}
function addAutoselect() {
const key = prompt('Enter autoselect key (e.g., "autoselect", "smart-select"):');
async function addAutoselect() {
const key = await showPrompt('Enter autoselect key (e.g., "autoselect", "smart-select"):', '', 'e.g. autoselect', 'Add Autoselect');
if (!key) {
return;
}
if (autoselectConfig[key]) {
alert('Autoselect key already exists');
showAlert('Autoselect key already exists', 'Duplicate Key', '⚠️', 'warn');
return;
}
......@@ -338,8 +338,8 @@ function addAutoselect() {
renderAutoselectList();
}
function removeAutoselect(key) {
if (confirm(`Remove autoselect "${key}"?`)) {
async function removeAutoselect(key) {
if (await showDangerConfirm(`Remove autoselect "${key}"?`, 'Remove Autoselect')) {
delete autoselectConfig[key];
expandedAutoselects.delete(key);
renderAutoselectList();
......@@ -375,8 +375,8 @@ function addAutoselectModel(autoselectKey) {
renderAutoselectModels(autoselectKey);
}
function removeAutoselectModel(autoselectKey, index) {
if (confirm('Remove this model?')) {
async function removeAutoselectModel(autoselectKey, index) {
if (await showDangerConfirm('Remove this model?', 'Remove Model')) {
autoselectConfig[autoselectKey].available_models.splice(index, 1);
renderAutoselectModels(autoselectKey);
}
......@@ -399,10 +399,10 @@ async function saveAutoselect() {
if (response.ok) {
window.location.href = autoselectData.successUrl;
} else {
alert('Error saving configuration');
showAlert('Error saving configuration', 'Error', '❌', 'danger');
}
} catch (error) {
alert('Error: ' + error.message);
showAlert('Error: ' + error.message, 'Error', '❌', 'danger');
}
}
......
......@@ -8,7 +8,7 @@
<div style="display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: 15px;">
<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;">
All subscription renewals and payments are automatically charged from your wallet first.
</div>
......@@ -315,11 +315,12 @@ document.addEventListener('DOMContentLoaded', function() {
if (!methodId) {
console.error('No method ID found');
alert('Error: Payment method ID not found');
showAlert('Error: Payment method ID not found', 'Error', '❌', 'danger');
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}`);
fetch(`/dashboard/billing/payment-methods/${methodId}`, {
......@@ -335,17 +336,16 @@ document.addEventListener('DOMContentLoaded', function() {
.then(data => {
console.log('Response data:', data);
if (data.success) {
alert('Payment method deleted successfully');
window.location.reload();
showAlert('Payment method deleted successfully', 'Done', '✅', 'info').then(() => window.location.reload());
} else {
alert('Error: ' + (data.error || 'Failed to delete payment method'));
showAlert('Error: ' + (data.error || 'Failed to delete payment method'), 'Error', '❌', 'danger');
}
})
.catch(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() {
if (!methodId) {
console.error('No method ID found');
alert('Error: Payment method ID not found');
showAlert('Error: Payment method ID not found', 'Error', '❌', 'danger');
return;
}
......@@ -382,15 +382,14 @@ document.addEventListener('DOMContentLoaded', function() {
.then(data => {
console.log('Response data:', data);
if (data.success) {
alert('Payment method set as default');
window.location.reload();
showAlert('Payment method set as default', 'Done', '✅', 'info').then(() => window.location.reload());
} 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 => {
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 @@
</style>
<script>
function confirmDeletion() {
async function confirmDeletion() {
const confirmation = document.getElementById('confirmation').value;
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 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>
{% endblock %}
......@@ -19,55 +19,94 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
{% block title %}Overview - AISBF Dashboard{% endblock %}
{% 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;">
<div style="background: #3498db; color: white; padding: 20px; border-radius: 8px;">
<h3 style="font-size: 16px; margin-bottom: 10px;">Providers</h3>
<p style="font-size: 32px; font-weight: bold;">{{ providers_count }}</p>
.info-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 20px; margin-bottom: 32px; }
.info-card { background: #1a1a2e; border: 1px solid #0f3460; border-radius: 10px; padding: 20px; }
.info-card h4 { font-size: .8em; text-transform: uppercase; letter-spacing: .08em; color: #a0a0a0; margin-bottom: 14px; }
.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 style="background: #2ecc71; color: white; padding: 20px; border-radius: 8px;">
<h3 style="font-size: 16px; margin-bottom: 10px;">Rotations</h3>
<p style="font-size: 32px; font-weight: bold;">{{ rotations_count }}</p>
<div class="stat-card">
<div class="stat-icon red"><i class="fas fa-brain" style="color:#e94560;"></i></div>
<div>
<div class="stat-value" style="color:#e94560;">{{ autoselect_count }}</div>
<div class="stat-label">Autoselects</div>
</div>
</div>
<div style="background: #e74c3c; color: white; padding: 20px; border-radius: 8px;">
<h3 style="font-size: 16px; margin-bottom: 10px;">Autoselect</h3>
<p style="font-size: 32px; font-weight: bold;">{{ autoselect_count }}</p>
<div class="stat-card">
<div class="stat-icon purple"><i class="fas fa-users" style="color:#9b59b6;"></i></div>
<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>
<h3 style="margin-bottom: 15px;">Server Information</h3>
<table>
<tr>
<th>Setting</th>
<th>Value</th>
</tr>
<tr>
<td>Host</td>
<td>{{ server_config.host }}</td>
</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>
<!-- Info cards -->
<div class="info-grid">
<div class="info-card">
<h4><i class="fas fa-server"></i> &nbsp;Server</h4>
<div class="info-row"><span>Host</span><span class="val">{{ server_config.host }}</span></div>
<div class="info-row"><span>Port</span><span class="val">{{ server_config.port }}</span></div>
<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>
<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>
</div>
<h3 style="margin-top: 30px; margin-bottom: 15px;">Quick Actions</h3>
<div style="display: flex; gap: 10px; flex-wrap: wrap;">
<a href="{{ url_for(request, '/dashboard/providers') }}" class="btn">Manage Providers</a>
<a href="{{ url_for(request, '/dashboard/rotations') }}" class="btn">Manage Rotations</a>
<a href="{{ url_for(request, '/dashboard/autoselect') }}" class="btn">Manage Autoselect</a>
<a href="{{ url_for(request, '/dashboard/prompts') }}" class="btn">Manage Prompts</a>
<a href="{{ url_for(request, '/dashboard/rate-limits') }}" class="btn">Rate Limits</a>
<a href="{{ url_for(request, '/dashboard/response-cache') }}" class="btn">Response Cache</a>
<a href="{{ url_for(request, '/dashboard/settings') }}" class="btn btn-secondary">Server Settings</a>
<div class="info-card">
<h4><i class="fas fa-bolt"></i> &nbsp;Quick Actions</h4>
<div class="quick-actions" style="margin-top:4px;">
<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/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/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/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/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/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>
{% endblock %}
......@@ -143,7 +143,7 @@ function openOAuthPopup(url) {
window.removeEventListener('message', messageListener);
// Show error
alert(event.data.error || 'Authentication failed');
showAlert(event.data.error || 'Authentication failed', 'Authentication Error', '❌', 'danger');
}
};
......
......@@ -216,8 +216,9 @@
{% block extra_js %}
<script>
function subscribeToTier(tierId) {
if (confirm('Are you sure you want to upgrade to this tier?')) {
async function subscribeToTier(tierId) {
const ok = await showConfirm('Are you sure you want to upgrade to this tier?', 'Upgrade Plan');
if (ok) {
fetch(url_for(`/api/subscribe/${tierId}`), {
method: 'POST',
headers: {
......@@ -241,8 +242,9 @@ function subscribeToTier(tierId) {
}
}
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.')) {
async function downgradeToFree() {
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'), {
method: 'POST',
headers: {
......
......@@ -72,9 +72,10 @@ function switchPrompt() {
}
}
function resetPrompt() {
async function resetPrompt() {
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, {
method: 'POST',
headers: {
......
......@@ -134,7 +134,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
{% block extra_js %}
<script>
let providersData = {{ providers_json | safe }};
let providersData = {{ providers_json | replace("</script>", "<\\/script>") | safe }};
let expandedProviders = new Set();
// Chunk size: 512KB chunks for maximum compatibility with restrictive proxies
......@@ -1603,8 +1603,8 @@ async function pollKiloAuth(providerKey, deviceCode) {
// This function is no longer needed as polling is handled in authenticateKilo
}
function removeProvider(key) {
if (confirm(`Remove provider "${key}"?`)) {
async function removeProvider(key) {
if (await showDangerConfirm(`Remove provider "${key}"?`, 'Remove Provider')) {
delete providersData[key];
expandedProviders.delete(key);
renderProvidersList();
......@@ -1684,8 +1684,8 @@ function addModel(key) {
renderModels(key);
}
function removeModel(providerKey, index) {
if (confirm('Remove this model?')) {
async function removeModel(providerKey, index) {
if (await showDangerConfirm('Remove this model?', 'Remove Model')) {
providersData[providerKey].models.splice(index, 1);
renderModels(providerKey);
}
......@@ -1696,12 +1696,12 @@ async function confirmAddProvider() {
const type = document.getElementById('new-provider-type').value;
if (!key) {
alert('Please enter a provider key');
showAlert('Please enter a provider key', 'Missing Key', '⚠️', 'warn');
return;
}
if (providersData[key]) {
alert('Provider key already exists');
showAlert('Provider key already exists', 'Duplicate Key', '⚠️', 'warn');
return;
}
......@@ -1836,10 +1836,10 @@ async function saveProviders() {
if (response.ok) {
window.location.href = '/dashboard/providers?success=1';
} else {
alert('Error saving configuration');
showAlert('Error saving configuration', 'Error', '❌', 'danger');
}
} catch (error) {
alert('Error: ' + error.message);
showAlert('Error: ' + error.message, 'Error', '❌', 'danger');
}
}
......
......@@ -169,9 +169,8 @@ async function loadRateLimits() {
}
async function resetRateLimiter(providerId) {
if (!confirm(`Reset rate limiter for ${providerId}?`)) {
return;
}
const ok = await showConfirm(`Reset rate limiter for ${providerId}?`, 'Reset Rate Limiter');
if (!ok) return;
try {
const response = await fetch(`/dashboard/rate-limits/${providerId}/reset`, {
......@@ -180,20 +179,19 @@ async function resetRateLimiter(providerId) {
const data = await response.json();
if (data.success) {
alert(data.message);
showAlert(data.message, 'Done', '✅', 'info');
loadRateLimits();
} else {
alert('Error: ' + data.error);
showAlert('Error: ' + data.error, 'Error', '❌', 'danger');
}
} catch (error) {
alert('Error: ' + error.message);
showAlert('Error: ' + error.message, 'Error', '❌', 'danger');
}
}
async function clearAllRateLimiters() {
if (!confirm('Reset all rate limiters? This will clear all learned rate limits.')) {
return;
}
const ok = await showDangerConfirm('Reset all rate limiters? This will clear all learned rate limits.', 'Reset All');
if (!ok) return;
const content = document.getElementById('rate-limits-content');
const providerIds = [];
......@@ -213,10 +211,10 @@ async function clearAllRateLimiters() {
}
}
alert('All rate limiters reset successfully');
showAlert('All rate limiters reset successfully', 'Done', '✅', 'info');
loadRateLimits();
} catch (error) {
alert('Error: ' + error.message);
showAlert('Error: ' + error.message, 'Error', '❌', 'danger');
}
}
......
......@@ -122,29 +122,24 @@ function refreshStats() {
location.reload();
}
function clearCache() {
if (!confirm('Are you sure you want to clear the entire response cache? This action cannot be undone.')) {
return;
}
async function clearCache() {
const ok = await showDangerConfirm('Are you sure you want to clear the entire response cache? This action cannot be undone.', 'Clear Cache');
if (!ok) return;
fetch('{{ url_for(request, "/dashboard/response-cache/clear") }}', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
}
})
.then(response => response.json())
.then(data => {
try {
const response = await fetch('{{ url_for(request, "/dashboard/response-cache/clear") }}', {
method: 'POST',
headers: {'Content-Type': 'application/json'}
});
const data = await response.json();
if (data.success) {
alert('Cache cleared successfully!');
location.reload();
showAlert('Cache cleared successfully!', 'Done', '✅', 'info').then(() => location.reload());
} else {
alert('Error clearing cache: ' + (data.error || 'Unknown error'));
showAlert('Error clearing cache: ' + (data.error || 'Unknown error'), 'Error', '❌', 'danger');
}
})
.catch(error => {
alert('Error clearing cache: ' + error);
});
} catch (error) {
showAlert('Error clearing cache: ' + error, 'Error', '❌', 'danger');
}
}
</script>
......
......@@ -268,10 +268,11 @@ function updateGlobalNotify(value) {
rotationsConfig.notifyerrors = value;
}
function addRotation() {
const key = prompt('Enter rotation key (e.g., "coding", "general"):');
if (!key || rotationsConfig.rotations[key]) {
alert('Invalid or duplicate rotation key');
async function addRotation() {
const key = await showPrompt('Enter rotation key (e.g., "coding", "general"):', '', 'rotation-key', 'Add Rotation');
if (!key) return;
if (rotationsConfig.rotations[key]) {
showAlert('Rotation key already exists', 'Duplicate Key', '⚠️', 'warn');
return;
}
......@@ -290,8 +291,8 @@ function addRotation() {
renderRotationsList();
}
function removeRotation(key) {
if (confirm(`Remove rotation "${key}"?`)) {
async function removeRotation(key) {
if (await showDangerConfirm(`Remove rotation "${key}"?`, 'Remove Rotation')) {
delete rotationsConfig.rotations[key];
expandedRotations.delete(key);
renderRotationsList();
......@@ -327,8 +328,8 @@ function addRotationProvider(rotationKey) {
renderRotationProviders(rotationKey);
}
function removeRotationProvider(rotationKey, providerIndex) {
if (confirm('Remove this provider?')) {
async function removeRotationProvider(rotationKey, providerIndex) {
if (await showDangerConfirm('Remove this provider?', 'Remove Provider')) {
rotationsConfig.rotations[rotationKey].providers.splice(providerIndex, 1);
renderRotationProviders(rotationKey);
}
......@@ -353,8 +354,8 @@ function addRotationModel(rotationKey, providerIndex) {
renderRotationModels(rotationKey, providerIndex);
}
function removeRotationModel(rotationKey, providerIndex, modelIndex) {
if (confirm('Remove this model?')) {
async function removeRotationModel(rotationKey, providerIndex, modelIndex) {
if (await showDangerConfirm('Remove this model?', 'Remove Model')) {
rotationsConfig.rotations[rotationKey].providers[providerIndex].models.splice(modelIndex, 1);
renderRotationModels(rotationKey, providerIndex);
}
......@@ -393,10 +394,10 @@ async function saveRotations() {
if (response.ok) {
window.location.href = rotationsData.successUrl;
} else {
alert('Error saving configuration');
showAlert('Error saving configuration', 'Error', '❌', 'danger');
}
} 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) {
const password = document.getElementById('password').value;
const confirmPassword = document.getElementById('confirm_password').value;
if (password !== confirmPassword) {
e.preventDefault();
alert('Passwords do not match!');
return false;
}
const check = (msg) => { e.preventDefault(); showAlert(msg, 'Validation Error', '⚠️', 'warn'); return false; };
// Check password strength
if (password.length < 8) {
e.preventDefault();
alert('Password must be at least 8 characters long!');
return false;
}
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;
}
if (password !== confirmPassword) return check('Passwords do not match!');
if (password.length < 8) return check('Password must be at least 8 characters long!');
if (!/[a-z]/.test(password)) return check('Password must contain at least one lowercase letter!');
if (!/[A-Z]/.test(password)) return check('Password must contain at least one uppercase letter!');
if (!/\d/.test(password)) return check('Password must contain at least one number!');
return true;
});
......@@ -189,7 +166,7 @@ function openOAuthPopup(url) {
window.removeEventListener('message', messageListener);
// Show error
alert(event.data.error || 'Authentication failed');
showAlert(event.data.error || 'Authentication failed', 'Authentication Error', '❌', 'danger');
}
};
......
......@@ -315,14 +315,14 @@ function renderAutoselectModels(autoselectKey) {
});
}
function addAutoselect() {
const key = prompt('Enter autoselect key (e.g., "autoselect", "smart-select"):');
async function addAutoselect() {
const key = await showPrompt('Enter autoselect key (e.g., "autoselect", "smart-select"):', '', 'e.g. autoselect', 'Add Autoselect');
if (!key) {
return;
}
if (autoselectConfig[key]) {
alert('Autoselect key already exists');
showAlert('Autoselect key already exists', 'Duplicate Key', '⚠️', 'warn');
return;
}
......@@ -338,8 +338,8 @@ function addAutoselect() {
renderAutoselectList();
}
function removeAutoselect(key) {
if (confirm(`Remove autoselect "${key}"?`)) {
async function removeAutoselect(key) {
if (await showDangerConfirm(`Remove autoselect "${key}"?`, 'Remove Autoselect')) {
delete autoselectConfig[key];
expandedAutoselects.delete(key);
renderAutoselectList();
......@@ -375,8 +375,8 @@ function addAutoselectModel(autoselectKey) {
renderAutoselectModels(autoselectKey);
}
function removeAutoselectModel(autoselectKey, index) {
if (confirm('Remove this model?')) {
async function removeAutoselectModel(autoselectKey, index) {
if (await showDangerConfirm('Remove this model?', 'Remove Model')) {
autoselectConfig[autoselectKey].available_models.splice(index, 1);
renderAutoselectModels(autoselectKey);
}
......@@ -399,10 +399,10 @@ async function saveAutoselect() {
if (response.ok) {
window.location.href = autoselectData.successUrl;
} else {
alert('Error saving configuration');
showAlert('Error saving configuration', 'Error', '❌', 'danger');
}
} catch (error) {
alert('Error: ' + error.message);
showAlert('Error: ' + error.message, 'Error', '❌', 'danger');
}
}
......
This diff is collapsed.
......@@ -256,7 +256,7 @@ function showToast(message, type) {
let providersData = {};
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
rawProviders.forEach(provider => {
......@@ -1705,8 +1705,8 @@ async function pollKiloAuth(providerKey, deviceCode) {
// This function is no longer needed as polling is handled in authenticateKilo
}
function removeProvider(key) {
if (confirm(`Remove provider "${key}"?`)) {
async function removeProvider(key) {
if (await showDangerConfirm(`Remove provider "${key}"?`, 'Remove Provider')) {
delete providersData[key];
expandedProviders.delete(key);
renderProvidersList();
......@@ -1786,8 +1786,8 @@ function addModel(key) {
renderModels(key);
}
function removeModel(providerKey, index) {
if (confirm('Remove this model?')) {
async function removeModel(providerKey, index) {
if (await showDangerConfirm('Remove this model?', 'Remove Model')) {
providersData[providerKey].models.splice(index, 1);
renderModels(providerKey);
}
......@@ -1798,12 +1798,12 @@ async function confirmAddProvider() {
const type = document.getElementById('new-provider-type').value;
if (!key) {
alert('Please enter a provider key');
showAlert('Please enter a provider key', 'Missing Key', '⚠️', 'warn');
return;
}
if (providersData[key]) {
alert('Provider key already exists');
showAlert('Provider key already exists', 'Duplicate Key', '⚠️', 'warn');
return;
}
......@@ -1931,10 +1931,10 @@ async function saveProviders() {
if (response.ok) {
window.location.href = '/dashboard/providers?success=1';
} else {
alert('Error saving configuration');
showAlert('Error saving configuration', 'Error', '❌', 'danger');
}
} catch (error) {
alert('Error: ' + error.message);
showAlert('Error: ' + error.message, 'Error', '❌', 'danger');
}
}
......
......@@ -259,10 +259,11 @@ function renderRotationModels(rotationKey, providerIndex) {
});
}
function addRotation() {
const key = prompt('Enter rotation key (e.g., "coding", "general"):');
if (!key || rotationsConfig.rotations[key]) {
alert('Invalid or duplicate rotation key');
async function addRotation() {
const key = await showPrompt('Enter rotation key (e.g., "coding", "general"):', '', 'rotation-key', 'Add Rotation');
if (!key) return;
if (rotationsConfig.rotations[key]) {
showAlert('Rotation key already exists', 'Duplicate Key', '⚠️', 'warn');
return;
}
......@@ -281,8 +282,8 @@ function addRotation() {
renderRotationsList();
}
function removeRotation(key) {
if (confirm(`Remove rotation "${key}"?`)) {
async function removeRotation(key) {
if (await showDangerConfirm(`Remove rotation "${key}"?`, 'Remove Rotation')) {
delete rotationsConfig.rotations[key];
expandedRotations.delete(key);
renderRotationsList();
......@@ -318,8 +319,8 @@ function addRotationProvider(rotationKey) {
renderRotationProviders(rotationKey);
}
function removeRotationProvider(rotationKey, providerIndex) {
if (confirm('Remove this provider?')) {
async function removeRotationProvider(rotationKey, providerIndex) {
if (await showDangerConfirm('Remove this provider?', 'Remove Provider')) {
rotationsConfig.rotations[rotationKey].providers.splice(providerIndex, 1);
renderRotationProviders(rotationKey);
}
......@@ -344,8 +345,8 @@ function addRotationModel(rotationKey, providerIndex) {
renderRotationModels(rotationKey, providerIndex);
}
function removeRotationModel(rotationKey, providerIndex, modelIndex) {
if (confirm('Remove this model?')) {
async function removeRotationModel(rotationKey, providerIndex, modelIndex) {
if (await showDangerConfirm('Remove this model?', 'Remove Model')) {
rotationsConfig.rotations[rotationKey].providers[providerIndex].models.splice(modelIndex, 1);
renderRotationModels(rotationKey, providerIndex);
}
......@@ -384,10 +385,10 @@ async function saveRotations() {
if (response.ok) {
window.location.href = rotationsData.successUrl;
} else {
alert('Error saving configuration');
showAlert('Error saving configuration', 'Error', '❌', 'danger');
}
} catch (error) {
alert('Error: ' + error.message);
showAlert('Error: ' + error.message, 'Error', '❌', 'danger');
}
}
......
This diff is collapsed.
......@@ -688,7 +688,7 @@ function clearSelection() {
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));
if (selectedIds.length === 0) {
......@@ -696,8 +696,9 @@ function performBulkAction(action, description, destructive = false, extraData =
return;
}
if (destructive && !confirm(`Are you sure you want to ${description}? This action cannot be undone.`)) {
return;
if (destructive) {
const ok = await showDangerConfirm(`Are you sure you want to ${description}? This action cannot be undone.`, 'Confirm Action');
if (!ok) return;
}
// Show loading state
......@@ -754,9 +755,10 @@ function closeEditModal() {
document.getElementById('edit-modal').style.display = 'none';
}
function toggleUserStatus(userId, currentStatus) {
async function toggleUserStatus(userId, currentStatus) {
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', {
method: 'POST',
headers: {'Content-Type': 'application/json'}
......@@ -766,17 +768,18 @@ function toggleUserStatus(userId, currentStatus) {
if (data.success) {
location.reload();
} else {
alert(data.error || 'Failed to toggle user status');
showAlert(data.error || 'Failed to toggle user status', 'Error', '❌', 'danger');
}
})
.catch(error => {
alert('Error: ' + error);
showAlert('Error: ' + error, 'Error', '❌', 'danger');
});
}
}
function deleteUser(userId, username) {
if (confirm('Are you sure you want to delete user "' + username + '"? This action cannot be undone.')) {
async function deleteUser(userId, username) {
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', {
method: 'POST',
headers: {'Content-Type': 'application/json'}
......@@ -786,11 +789,11 @@ function deleteUser(userId, username) {
if (data.success) {
location.reload();
} else {
alert(data.error || 'Failed to delete user');
showAlert(data.error || 'Failed to delete user', 'Error', '❌', 'danger');
}
})
.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