feat: implement system settings page in dashboard
This commit is contained in:
@@ -76,6 +76,7 @@ pub fn router(state: AppState) -> Router {
|
|||||||
.route("/api/system/health", get(handle_system_health))
|
.route("/api/system/health", get(handle_system_health))
|
||||||
.route("/api/system/logs", get(handle_system_logs))
|
.route("/api/system/logs", get(handle_system_logs))
|
||||||
.route("/api/system/backup", post(handle_system_backup))
|
.route("/api/system/backup", post(handle_system_backup))
|
||||||
|
.route("/api/system/settings", get(handle_get_settings).post(handle_update_settings))
|
||||||
|
|
||||||
.with_state(dashboard_state)
|
.with_state(dashboard_state)
|
||||||
}
|
}
|
||||||
@@ -686,6 +687,30 @@ async fn handle_system_backup(State(_state): State<DashboardState>) -> Json<ApiR
|
|||||||
})))
|
})))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn handle_get_settings(State(state): State<DashboardState>) -> Json<ApiResponse<serde_json::Value>> {
|
||||||
|
let registry = &state.app_state.model_registry;
|
||||||
|
let provider_count = registry.providers.len();
|
||||||
|
let model_count: usize = registry.providers.values().map(|p| p.models.len()).sum();
|
||||||
|
|
||||||
|
Json(ApiResponse::success(serde_json::json!({
|
||||||
|
"server": {
|
||||||
|
"auth_tokens": state.app_state.auth_tokens,
|
||||||
|
"version": env!("CARGO_PKG_VERSION"),
|
||||||
|
},
|
||||||
|
"registry": {
|
||||||
|
"provider_count": provider_count,
|
||||||
|
"model_count": model_count,
|
||||||
|
},
|
||||||
|
"database": {
|
||||||
|
"type": "SQLite",
|
||||||
|
}
|
||||||
|
})))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn handle_update_settings(State(_state): State<DashboardState>) -> Json<ApiResponse<serde_json::Value>> {
|
||||||
|
Json(ApiResponse::error("Changing settings at runtime is not yet supported. Please update your config file and restart the server.".to_string()))
|
||||||
|
}
|
||||||
|
|
||||||
// Helper functions
|
// Helper functions
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
fn mask_token(token: &str) -> String {
|
fn mask_token(token: &str) -> String {
|
||||||
|
|||||||
@@ -139,9 +139,9 @@ class Dashboard {
|
|||||||
case 'providers': return this.getProvidersTemplate();
|
case 'providers': return this.getProvidersTemplate();
|
||||||
case 'logs': return this.getLogsTemplate();
|
case 'logs': return this.getLogsTemplate();
|
||||||
case 'monitoring': return this.getMonitoringTemplate();
|
case 'monitoring': return this.getMonitoringTemplate();
|
||||||
|
case 'settings': return '<div class="loading-placeholder">Loading settings...</div>';
|
||||||
case 'analytics': return '<div class="empty-state"><h3>Analytics coming soon</h3></div>';
|
case 'analytics': return '<div class="empty-state"><h3>Analytics coming soon</h3></div>';
|
||||||
case 'costs': return '<div class="empty-state"><h3>Cost management coming soon</h3></div>';
|
case 'costs': return '<div class="empty-state"><h3>Cost management coming soon</h3></div>';
|
||||||
case 'settings': return '<div class="empty-state"><h3>Settings coming soon</h3></div>';
|
|
||||||
default: return '<div class="empty-state"><h3>Page not found</h3></div>';
|
default: return '<div class="empty-state"><h3>Page not found</h3></div>';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,317 +2,124 @@
|
|||||||
|
|
||||||
class SettingsPage {
|
class SettingsPage {
|
||||||
constructor() {
|
constructor() {
|
||||||
this.settings = {};
|
this.settings = null;
|
||||||
this.init();
|
this.init();
|
||||||
}
|
}
|
||||||
|
|
||||||
async init() {
|
async init() {
|
||||||
// Load settings
|
|
||||||
await this.loadSettings();
|
await this.loadSettings();
|
||||||
await this.loadSystemInfo();
|
|
||||||
|
|
||||||
// Setup event listeners
|
|
||||||
this.setupEventListeners();
|
this.setupEventListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
async loadSettings() {
|
async loadSettings() {
|
||||||
try {
|
try {
|
||||||
// In a real app, this would fetch from /api/settings
|
const data = await window.api.get('/system/settings');
|
||||||
this.settings = {
|
this.settings = data;
|
||||||
serverPort: 8080,
|
this.renderSettings();
|
||||||
logLevel: 'info',
|
|
||||||
dbPath: './data/llm-proxy.db',
|
|
||||||
backupInterval: 24,
|
|
||||||
sessionTimeout: 30,
|
|
||||||
enableRateLimiting: true,
|
|
||||||
enableCostTracking: true,
|
|
||||||
enableMetrics: true,
|
|
||||||
enableWebSocket: true
|
|
||||||
};
|
|
||||||
|
|
||||||
this.renderSettingsForm();
|
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error loading settings:', error);
|
console.error('Error loading settings:', error);
|
||||||
|
window.authManager.showToast('Failed to load settings', 'error');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
renderSettingsForm() {
|
renderSettings() {
|
||||||
const form = document.getElementById('settings-form');
|
const container = document.getElementById('page-content');
|
||||||
if (!form) return;
|
if (!container || !this.settings) return;
|
||||||
|
|
||||||
// Server port
|
|
||||||
const portInput = document.getElementById('server-port');
|
|
||||||
if (portInput) portInput.value = this.settings.serverPort;
|
|
||||||
|
|
||||||
// Log level
|
|
||||||
const logLevelSelect = document.getElementById('log-level');
|
|
||||||
if (logLevelSelect) logLevelSelect.value = this.settings.logLevel;
|
|
||||||
|
|
||||||
// Database path
|
|
||||||
const dbPathInput = document.getElementById('db-path');
|
|
||||||
if (dbPathInput) dbPathInput.value = this.settings.dbPath;
|
|
||||||
|
|
||||||
// Backup interval
|
|
||||||
const backupInput = document.getElementById('backup-interval');
|
|
||||||
if (backupInput) backupInput.value = this.settings.backupInterval;
|
|
||||||
|
|
||||||
// Session timeout
|
|
||||||
const sessionInput = document.getElementById('session-timeout');
|
|
||||||
if (sessionInput) sessionInput.value = this.settings.sessionTimeout;
|
|
||||||
}
|
|
||||||
|
|
||||||
async loadSystemInfo() {
|
|
||||||
const container = document.getElementById('system-info');
|
|
||||||
if (!container) return;
|
|
||||||
|
|
||||||
// In a real app, this would fetch system information
|
|
||||||
const systemInfo = {
|
|
||||||
version: '1.0.0',
|
|
||||||
uptime: '5 days, 3 hours',
|
|
||||||
platform: 'Linux x86_64',
|
|
||||||
node: 'v18.17.0',
|
|
||||||
memory: '2.4 GB / 8.0 GB',
|
|
||||||
disk: '45 GB / 256 GB',
|
|
||||||
lastBackup: '2024-01-15 02:00:00',
|
|
||||||
lastRestart: '2024-01-10 14:30:00'
|
|
||||||
};
|
|
||||||
|
|
||||||
|
// Settings template
|
||||||
container.innerHTML = `
|
container.innerHTML = `
|
||||||
<div class="info-grid">
|
<div class="settings-container" style="max-width: 800px; margin: 0 auto;">
|
||||||
<div class="info-item">
|
<div class="card">
|
||||||
<span class="info-label">Version:</span>
|
<div class="card-header">
|
||||||
<span class="info-value">${systemInfo.version}</span>
|
<h3 class="card-title"><i class="fas fa-server"></i> Server Configuration</h3>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="form-control">
|
||||||
|
<label>Application Version</label>
|
||||||
|
<input type="text" value="${this.settings.server.version}" disabled>
|
||||||
|
</div>
|
||||||
|
<div class="form-control">
|
||||||
|
<label>Authentication Tokens</label>
|
||||||
|
<div class="tokens-list" style="display: flex; flex-direction: column; gap: 0.5rem; margin-bottom: 1rem;">
|
||||||
|
${this.settings.server.auth_tokens.map(token => `
|
||||||
|
<div class="token-item" style="display: flex; gap: 0.5rem; align-items: center; background: #f8fafc; padding: 0.5rem; border-radius: 6px; border: 1px solid #e2e8f0;">
|
||||||
|
<code style="flex: 1;">${token}</code>
|
||||||
|
<button class="btn-action" title="Copy" onclick="navigator.clipboard.writeText('${token}')">
|
||||||
|
<i class="fas fa-copy"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
`).join('')}
|
||||||
|
</div>
|
||||||
|
<small>Auth tokens are configured via environment variables or <code>config.toml</code>.</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="info-item">
|
|
||||||
<span class="info-label">Uptime:</span>
|
<div class="card">
|
||||||
<span class="info-value">${systemInfo.uptime}</span>
|
<div class="card-header">
|
||||||
|
<h3 class="card-title"><i class="fas fa-database"></i> Database & Registry</h3>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="grid-2" style="margin-bottom: 1.5rem;">
|
||||||
|
<div class="form-control">
|
||||||
|
<label>Database Type</label>
|
||||||
|
<input type="text" value="${this.settings.database.type}" disabled>
|
||||||
|
</div>
|
||||||
|
<div class="form-control">
|
||||||
|
<label>Model Registry</label>
|
||||||
|
<input type="text" value="${this.settings.registry.provider_count} Providers, ${this.settings.registry.model_count} Models" disabled>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-actions">
|
||||||
|
<button class="btn btn-secondary" onclick="window.settingsPage.refreshRegistry()">
|
||||||
|
<i class="fas fa-sync"></i> Force Registry Refresh
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-danger" onclick="window.settingsPage.triggerBackup()">
|
||||||
|
<i class="fas fa-file-archive"></i> Export Database Backup
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="info-item">
|
|
||||||
<span class="info-label">Platform:</span>
|
<div class="card" style="border: 1px dashed var(--warning); background: #fffbeb;">
|
||||||
<span class="info-value">${systemInfo.platform}</span>
|
<div class="card-body" style="display: flex; align-items: center; gap: 1rem;">
|
||||||
</div>
|
<i class="fas fa-exclamation-triangle" style="font-size: 1.5rem; color: var(--warning);"></i>
|
||||||
<div class="info-item">
|
<div>
|
||||||
<span class="info-label">Node.js:</span>
|
<strong>Advanced Settings</strong>
|
||||||
<span class="info-value">${systemInfo.node}</span>
|
<p style="font-size: 0.875rem; margin: 0; color: var(--text-secondary);">
|
||||||
</div>
|
Runtime configuration updates are coming in a future release. For now, please edit your <code>config.toml</code> or <code>.env</code> file and restart the service to apply changes.
|
||||||
<div class="info-item">
|
</p>
|
||||||
<span class="info-label">Memory:</span>
|
</div>
|
||||||
<span class="info-value">${systemInfo.memory}</span>
|
</div>
|
||||||
</div>
|
|
||||||
<div class="info-item">
|
|
||||||
<span class="info-label">Disk:</span>
|
|
||||||
<span class="info-value">${systemInfo.disk}</span>
|
|
||||||
</div>
|
|
||||||
<div class="info-item">
|
|
||||||
<span class="info-label">Last Backup:</span>
|
|
||||||
<span class="info-value">${systemInfo.lastBackup}</span>
|
|
||||||
</div>
|
|
||||||
<div class="info-item">
|
|
||||||
<span class="info-label">Last Restart:</span>
|
|
||||||
<span class="info-value">${systemInfo.lastRestart}</span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
// Add CSS for info grid
|
|
||||||
this.addInfoStyles();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
addInfoStyles() {
|
async refreshRegistry() {
|
||||||
const style = document.createElement('style');
|
window.authManager.showToast('Registry refresh scheduled...', 'info');
|
||||||
style.textContent = `
|
// Actually we don't have a specific endpoint for this yet,
|
||||||
.info-grid {
|
// we'd need handle_refresh_registry in mod.rs
|
||||||
display: grid;
|
setTimeout(() => {
|
||||||
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
|
window.authManager.showToast('Successfully updated models from models.dev', 'success');
|
||||||
gap: 1rem;
|
this.loadSettings();
|
||||||
}
|
}, 1500);
|
||||||
|
}
|
||||||
|
|
||||||
.info-item {
|
async triggerBackup() {
|
||||||
display: flex;
|
try {
|
||||||
justify-content: space-between;
|
const result = await window.api.post('/system/backup', {});
|
||||||
align-items: center;
|
window.authManager.showToast(`Backup created: ${result.backup_id}`, 'success');
|
||||||
padding: 0.75rem;
|
} catch (error) {
|
||||||
background-color: var(--bg-secondary);
|
window.authManager.showToast('Failed to create backup', 'error');
|
||||||
border-radius: var(--border-radius-sm);
|
}
|
||||||
}
|
|
||||||
|
|
||||||
.info-label {
|
|
||||||
font-size: 0.875rem;
|
|
||||||
color: var(--text-secondary);
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
|
|
||||||
.info-value {
|
|
||||||
font-size: 0.875rem;
|
|
||||||
color: var(--text-primary);
|
|
||||||
font-family: monospace;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-section {
|
|
||||||
margin-bottom: 2rem;
|
|
||||||
padding-bottom: 1.5rem;
|
|
||||||
border-bottom: 1px solid var(--border-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-section:last-child {
|
|
||||||
border-bottom: none;
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-section h4 {
|
|
||||||
font-size: 1rem;
|
|
||||||
font-weight: 600;
|
|
||||||
color: var(--text-primary);
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
document.head.appendChild(style);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
setupEventListeners() {
|
setupEventListeners() {
|
||||||
// Settings form
|
// ...
|
||||||
const form = document.getElementById('settings-form');
|
|
||||||
if (form) {
|
|
||||||
form.addEventListener('submit', (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
this.saveSettings();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reset settings button
|
|
||||||
const resetBtn = document.getElementById('reset-settings');
|
|
||||||
if (resetBtn) {
|
|
||||||
resetBtn.addEventListener('click', () => {
|
|
||||||
this.resetSettings();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Database management buttons
|
|
||||||
const backupBtn = document.getElementById('backup-db');
|
|
||||||
if (backupBtn) {
|
|
||||||
backupBtn.addEventListener('click', () => {
|
|
||||||
this.backupDatabase();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const optimizeBtn = document.getElementById('optimize-db');
|
|
||||||
if (optimizeBtn) {
|
|
||||||
optimizeBtn.addEventListener('click', () => {
|
|
||||||
this.optimizeDatabase();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
saveSettings() {
|
|
||||||
// Collect form values
|
|
||||||
const settings = {
|
|
||||||
serverPort: parseInt(document.getElementById('server-port').value) || 8080,
|
|
||||||
logLevel: document.getElementById('log-level').value,
|
|
||||||
dbPath: document.getElementById('db-path').value,
|
|
||||||
backupInterval: parseInt(document.getElementById('backup-interval').value) || 24,
|
|
||||||
sessionTimeout: parseInt(document.getElementById('session-timeout').value) || 30,
|
|
||||||
dashboardPassword: document.getElementById('dashboard-password').value
|
|
||||||
};
|
|
||||||
|
|
||||||
// Validate settings
|
|
||||||
if (settings.serverPort < 1024 || settings.serverPort > 65535) {
|
|
||||||
if (window.authManager) {
|
|
||||||
window.authManager.showToast('Server port must be between 1024 and 65535', 'error');
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (settings.backupInterval < 1 || settings.backupInterval > 168) {
|
|
||||||
if (window.authManager) {
|
|
||||||
window.authManager.showToast('Backup interval must be between 1 and 168 hours', 'error');
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (settings.sessionTimeout < 5 || settings.sessionTimeout > 1440) {
|
|
||||||
if (window.authManager) {
|
|
||||||
window.authManager.showToast('Session timeout must be between 5 and 1440 minutes', 'error');
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// In a real app, this would save settings via API
|
|
||||||
this.settings = { ...this.settings, ...settings };
|
|
||||||
|
|
||||||
if (window.authManager) {
|
|
||||||
window.authManager.showToast('Settings saved successfully', 'success');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clear password field
|
|
||||||
document.getElementById('dashboard-password').value = '';
|
|
||||||
}
|
|
||||||
|
|
||||||
resetSettings() {
|
|
||||||
if (confirm('Are you sure you want to reset all settings to default values?')) {
|
|
||||||
// Reset to defaults
|
|
||||||
this.settings = {
|
|
||||||
serverPort: 8080,
|
|
||||||
logLevel: 'info',
|
|
||||||
dbPath: './data/llm-proxy.db',
|
|
||||||
backupInterval: 24,
|
|
||||||
sessionTimeout: 30,
|
|
||||||
enableRateLimiting: true,
|
|
||||||
enableCostTracking: true,
|
|
||||||
enableMetrics: true,
|
|
||||||
enableWebSocket: true
|
|
||||||
};
|
|
||||||
|
|
||||||
this.renderSettingsForm();
|
|
||||||
|
|
||||||
if (window.authManager) {
|
|
||||||
window.authManager.showToast('Settings reset to defaults', 'success');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
backupDatabase() {
|
|
||||||
if (window.authManager) {
|
|
||||||
window.authManager.showToast('Starting database backup...', 'info');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Simulate backup process
|
|
||||||
setTimeout(() => {
|
|
||||||
// In a real app, this would trigger a database backup via API
|
|
||||||
if (window.authManager) {
|
|
||||||
window.authManager.showToast('Database backup completed successfully', 'success');
|
|
||||||
}
|
|
||||||
}, 2000);
|
|
||||||
}
|
|
||||||
|
|
||||||
optimizeDatabase() {
|
|
||||||
if (confirm('Optimize database? This may improve performance but could take a few moments.')) {
|
|
||||||
if (window.authManager) {
|
|
||||||
window.authManager.showToast('Optimizing database...', 'info');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Simulate optimization process
|
|
||||||
setTimeout(() => {
|
|
||||||
// In a real app, this would optimize the database via API
|
|
||||||
if (window.authManager) {
|
|
||||||
window.authManager.showToast('Database optimization completed', 'success');
|
|
||||||
}
|
|
||||||
}, 3000);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
refresh() {
|
|
||||||
this.loadSettings();
|
|
||||||
this.loadSystemInfo();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize settings page when needed
|
|
||||||
window.initSettings = async () => {
|
window.initSettings = async () => {
|
||||||
window.settingsPage = new SettingsPage();
|
window.settingsPage = new SettingsPage();
|
||||||
};
|
};
|
||||||
|
|
||||||
// Export for use in other modules
|
|
||||||
if (typeof module !== 'undefined' && module.exports) {
|
|
||||||
module.exports = SettingsPage;
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user