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", {
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', '$')
}
)
......
......@@ -138,20 +138,21 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
});
function restartServer() {
if (confirm('Are you sure you want to restart the server? This will disconnect all active connections.')) {
showConfirm('Are you sure you want to restart the server? This will disconnect all active connections.', 'Restart Server', '🔄', 'warn').then(ok => {
if (!ok) return;
fetch('{{ url_for(request, "/dashboard/restart") }}', {
method: 'POST',
headers: {'Content-Type': 'application/json'}
})
.then(response => response.json())
.then(data => {
alert(data.message + ' The page will reload in 5 seconds.');
showAlert(data.message + ' The page will reload in 5 seconds.', 'Restarting');
setTimeout(() => window.location.reload(), 5000);
})
.catch(error => {
alert('Error restarting server: ' + error);
showAlert('Error restarting server: ' + error, 'Error', '❌', 'danger');
});
});
}
}
// Snake Game Easter Egg
......@@ -418,9 +419,10 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
function gameOver() {
clearInterval(gameLoop);
alert('Game Over! Score: ' + score + '\nPress OK to play again.');
showAlert('Game Over! Score: ' + score, 'Game Over 🐍').then(() => {
startScreen.classList.remove('hidden');
scoreDisplay.style.display = 'none';
});
}
// Keyboard controls
......@@ -465,6 +467,9 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
<h1>AISBF Dashboard <span style="font-size: 0.8em; font-weight: normal; opacity: 0.7;">v{{ __version__ }}</span></h1>
{% if request.session.logged_in %}
<div class="header-actions">
{% if is_aisbf_cloud %}
<button onclick="openContactModal()" class="btn btn-secondary">Help</button>
{% endif %}
<a href="{{ url_for(request, '/dashboard/docs') }}" class="btn btn-secondary">Docs</a>
<a href="{{ url_for(request, '/dashboard/about') }}" class="btn btn-secondary">About</a>
<a href="{{ url_for(request, '/dashboard/license') }}" class="btn btn-secondary">License</a>
......@@ -572,24 +577,20 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
box-shadow: 0 4px 15px rgba(245, 87, 108, 0.4);
}
.donate-modal {
/* ── Shared modal backdrop ── */
.donate-modal, .ui-modal-backdrop {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
top: 0; left: 0;
width: 100%; height: 100%;
background: rgba(0,0,0,0.7);
z-index: 1000;
align-items: center;
justify-content: center;
}
.donate-modal.active, .ui-modal-backdrop.active { display: flex; }
.donate-modal.active {
display: flex;
}
.donate-modal-content {
.donate-modal-content, .ui-modal-box {
background: #16213e;
padding: 30px;
border-radius: 12px;
......@@ -600,14 +601,14 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
border: 1px solid #0f3460;
}
.donate-modal-header {
.donate-modal-header, .ui-modal-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.donate-modal-header h3 {
.donate-modal-header h3, .ui-modal-header h3 {
color: #f5576c;
margin: 0;
}
......@@ -620,6 +621,20 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
cursor: pointer;
}
/* ── ui-modal specific ── */
.ui-modal-body { color: #e0e0e0; line-height: 1.5; margin-bottom: 20px; }
.ui-modal-input {
width: 100%; padding: 10px;
border: 1px solid #0f3460; border-radius: 4px;
font-size: 14px; background: #1a1a2e; color: #e0e0e0;
margin-top: 10px;
}
.ui-modal-footer { display: flex; gap: 10px; justify-content: flex-end; }
.ui-modal-icon { font-size: 2em; margin-bottom: 10px; display: block; text-align: center; }
.ui-modal-icon.warn { color: #f39c12; }
.ui-modal-icon.info { color: #3b82f6; }
.ui-modal-icon.danger { color: #e74c3c; }
.crypto-address {
background: #0f3460;
padding: 12px;
......@@ -909,6 +924,100 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
}
</script>
<!-- Global UI Modal System -->
<div class="ui-modal-backdrop" id="uiModal">
<div class="ui-modal-box" style="max-width:420px;">
<div class="ui-modal-header">
<h3 id="uiModalTitle">Notice</h3>
<button class="close-modal" id="uiModalClose">&times;</button>
</div>
<div class="ui-modal-body">
<span id="uiModalIcon" class="ui-modal-icon" style="display:none;"></span>
<div id="uiModalMsg"></div>
<input class="ui-modal-input" id="uiModalInput" type="text" style="display:none;" placeholder="">
</div>
<div class="ui-modal-footer" id="uiModalFooter"></div>
</div>
</div>
<script>
(function(){
const backdrop = document.getElementById('uiModal');
const titleEl = document.getElementById('uiModalTitle');
const iconEl = document.getElementById('uiModalIcon');
const msgEl = document.getElementById('uiModalMsg');
const inputEl = document.getElementById('uiModalInput');
const footerEl = document.getElementById('uiModalFooter');
const closeBtn = document.getElementById('uiModalClose');
let _resolve = null;
function open(opts) {
titleEl.textContent = opts.title || 'Notice';
msgEl.textContent = opts.message || '';
if (opts.icon) { iconEl.className = 'ui-modal-icon ' + (opts.iconClass||'info'); iconEl.textContent = opts.icon; iconEl.style.display='block'; }
else { iconEl.style.display='none'; }
inputEl.style.display = opts.prompt ? 'block' : 'none';
inputEl.value = opts.defaultValue || '';
inputEl.placeholder = opts.placeholder || '';
footerEl.innerHTML = '';
(opts.buttons||[]).forEach(b => {
const btn = document.createElement('button');
btn.textContent = b.label;
btn.className = 'btn ' + (b.cls||'btn-secondary');
btn.onclick = () => { close(); _resolve(b.value); };
footerEl.appendChild(btn);
});
backdrop.classList.add('active');
if (opts.prompt) setTimeout(()=>inputEl.focus(),50);
}
function close() { backdrop.classList.remove('active'); }
closeBtn.onclick = () => { close(); if(_resolve) _resolve(null); };
backdrop.addEventListener('click', e => { if(e.target===backdrop){ close(); if(_resolve) _resolve(null); } });
window.showAlert = function(message, title, icon, iconClass) {
return new Promise(res => {
_resolve = res;
open({ title: title||'Notice', message, icon: icon||'ℹ️', iconClass: iconClass||'info',
buttons: [{label:'OK', value:true, cls:'btn'}] });
});
};
window.showConfirm = function(message, title, icon, iconClass) {
return new Promise(res => {
_resolve = res;
open({ title: title||'Confirm', message, icon: icon||'⚠️', iconClass: iconClass||'warn',
buttons: [
{label:'Cancel', value:false, cls:'btn-secondary'},
{label:'Confirm', value:true, cls:'btn'}
] });
});
};
window.showDangerConfirm = function(message, title) {
return new Promise(res => {
_resolve = res;
open({ title: title||'Warning', message, icon:'🗑️', iconClass:'danger',
buttons: [
{label:'Cancel', value:false, cls:'btn-secondary'},
{label:'Delete', value:true, cls:'btn btn-danger'}
] });
});
};
window.showPrompt = function(message, defaultValue, placeholder, title) {
return new Promise(res => {
_resolve = v => res(v === null ? null : inputEl.value || null);
open({ title: title||'Input', message, prompt:true, defaultValue, placeholder,
buttons: [
{label:'Cancel', value:null, cls:'btn-secondary'},
{label:'OK', value:true, cls:'btn'}
] });
// OK button needs to read input value
const okBtn = footerEl.lastChild;
okBtn.onclick = () => { const v = inputEl.value.trim(); close(); res(v||null); };
});
};
})();
</script>
{% block extra_js %}{% endblock %}
</body>
</html>
......@@ -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 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>
<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 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 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") }}', {
try {
const response = await fetch('{{ url_for(request, "/dashboard/response-cache/clear") }}', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
}
})
.then(response => response.json())
.then(data => {
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) {
showAlert('Error clearing cache: ' + error, 'Error', '❌', 'danger');
}
})
.catch(error => {
alert('Error clearing cache: ' + error);
});
}
</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;
}
// 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;
}
const check = (msg) => { e.preventDefault(); showAlert(msg, 'Validation Error', '⚠️', 'warn'); 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