// AI Server Admin Dashboard JavaScript class AdminDashboard { constructor() { this.apiKey = this.getApiKey(); this.baseUrl = window.location.origin; this.init(); } getApiKey() { // JWT 토큰 사용 const token = localStorage.getItem('ai_admin_token'); console.log('Getting token:', token ? token.substring(0, 20) + '...' : 'No token found'); if (!token) { // 토큰이 없으면 로그인 페이지로 리다이렉트 console.log('No token, redirecting to login...'); window.location.href = '/login'; return null; } return token; } async init() { // 먼저 토큰 검증 if (!this.apiKey) { return; // getApiKey()에서 이미 리다이렉트됨 } // 토큰 유효성 검증 try { await this.apiRequest('/admin/verify-token'); console.log('Token verification successful'); } catch (error) { console.log('Token verification failed, redirecting to login'); localStorage.removeItem('ai_admin_token'); localStorage.removeItem('ai_admin_user'); window.location.href = '/login'; return; } this.updateCurrentTime(); setInterval(() => this.updateCurrentTime(), 1000); await this.loadUserInfo(); // Phase 3: Load user info await this.loadSystemStatus(); await this.loadModels(); await this.loadApiKeys(); await this.loadSystemStats(); // Phase 2 // Auto-refresh every 30 seconds setInterval(() => { this.loadSystemStatus(); this.loadModels(); this.loadSystemStats(); // Phase 2 }, 30000); } // Phase 3: User Management async loadUserInfo() { try { const userInfo = localStorage.getItem('ai_admin_user'); if (userInfo) { const user = JSON.parse(userInfo); document.getElementById('username').textContent = user.username; } else { // Verify token and get user info const response = await this.apiRequest('/admin/verify-token'); if (response.valid) { document.getElementById('username').textContent = response.user.username; localStorage.setItem('ai_admin_user', JSON.stringify(response.user)); } } } catch (error) { console.error('Failed to load user info:', error); // Token might be invalid, redirect to login window.location.href = '/login'; } } updateCurrentTime() { const now = new Date(); document.getElementById('current-time').textContent = now.toLocaleString('ko-KR', { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit' }); } async apiRequest(endpoint, options = {}) { const url = `${this.baseUrl}${endpoint}`; const defaultOptions = { headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${this.apiKey}` } }; console.log('API Request:', endpoint, 'with token:', this.apiKey ? this.apiKey.substring(0, 20) + '...' : 'No token'); try { const response = await fetch(url, { ...defaultOptions, ...options }); console.log('API Response:', response.status, response.statusText); if (!response.ok) { if (response.status === 401) { console.log('401 Unauthorized - clearing tokens and redirecting'); // JWT 토큰이 만료되었거나 유효하지 않음 localStorage.removeItem('ai_admin_token'); localStorage.removeItem('ai_admin_user'); window.location.href = '/login'; return; } const errorText = await response.text(); console.log('Error response:', errorText); throw new Error(`HTTP ${response.status}: ${response.statusText}`); } return await response.json(); } catch (error) { console.error('API Request failed:', error); throw error; } } async loadSystemStatus() { try { // Check AI Server status const healthResponse = await this.apiRequest('/health'); document.getElementById('server-status').textContent = 'Online'; document.getElementById('server-status').className = 'status-value'; // Check Ollama status try { const ollamaResponse = await this.apiRequest('/admin/ollama/status'); document.getElementById('ollama-status').textContent = ollamaResponse.status === 'online' ? 'Online' : 'Offline'; document.getElementById('ollama-status').className = `status-value ${ollamaResponse.status === 'online' ? '' : 'error'}`; } catch (error) { document.getElementById('ollama-status').textContent = 'Offline'; document.getElementById('ollama-status').className = 'status-value error'; } // Load active model try { const modelResponse = await this.apiRequest('/admin/models/active'); document.getElementById('active-model').textContent = modelResponse.model || 'None'; } catch (error) { document.getElementById('active-model').textContent = 'Unknown'; } // Load API call stats (placeholder) document.getElementById('api-calls').textContent = '0'; } catch (error) { console.error('Failed to load system status:', error); document.getElementById('server-status').textContent = 'Error'; document.getElementById('server-status').className = 'status-value error'; } } async loadModels() { try { const response = await this.apiRequest('/admin/models'); const models = response.models || []; const tbody = document.getElementById('models-tbody'); if (models.length === 0) { tbody.innerHTML = 'No models found'; return; } tbody.innerHTML = models.map(model => ` ${model.name} ${model.is_active ? 'Active' : ''} ${this.formatSize(model.size)} ${model.status || 'Unknown'} ${model.last_used ? new Date(model.last_used).toLocaleString('ko-KR') : 'Never'} `).join(''); } catch (error) { console.error('Failed to load models:', error); document.getElementById('models-tbody').innerHTML = 'Error loading models'; } } async loadApiKeys() { try { const response = await this.apiRequest('/admin/api-keys'); const apiKeys = response.api_keys || []; const container = document.getElementById('api-keys-list'); if (apiKeys.length === 0) { container.innerHTML = '
No API keys found
'; return; } container.innerHTML = apiKeys.map(key => `
${key.name || 'Unnamed Key'}
${this.maskApiKey(key.key)}
Created: ${new Date(key.created_at).toLocaleString('ko-KR')} | Uses: ${key.usage_count || 0}
`).join(''); } catch (error) { console.error('Failed to load API keys:', error); document.getElementById('api-keys-list').innerHTML = '
Error loading API keys
'; } } formatSize(bytes) { if (!bytes) return 'Unknown'; const sizes = ['B', 'KB', 'MB', 'GB', 'TB']; const i = Math.floor(Math.log(bytes) / Math.log(1024)); return Math.round(bytes / Math.pow(1024, i) * 100) / 100 + ' ' + sizes[i]; } maskApiKey(key) { if (!key) return 'Unknown'; if (key.length <= 8) return key; return key.substring(0, 4) + '...' + key.substring(key.length - 4); } async refreshModels() { document.getElementById('models-tbody').innerHTML = 'Refreshing models...'; await this.loadModels(); } async testModel(modelName) { try { const response = await this.apiRequest('/admin/models/test', { method: 'POST', body: JSON.stringify({ model: modelName }) }); alert(`Model test result:\n${response.result || 'Test completed successfully'}`); } catch (error) { alert(`Model test failed: ${error.message}`); } } async generateApiKey() { const name = prompt('Enter a name for the new API key:'); if (!name) return; try { const response = await this.apiRequest('/admin/api-keys', { method: 'POST', body: JSON.stringify({ name }) }); alert(`New API key created:\n${response.api_key}\n\nPlease save this key securely. It will not be shown again.`); await this.loadApiKeys(); } catch (error) { alert(`Failed to generate API key: ${error.message}`); } } async deleteApiKey(keyId) { if (!confirm('Are you sure you want to delete this API key?')) return; try { await this.apiRequest(`/admin/api-keys/${keyId}`, { method: 'DELETE' }); await this.loadApiKeys(); } catch (error) { alert(`Failed to delete API key: ${error.message}`); } } // Phase 2: System Monitoring async loadSystemStats() { try { const response = await this.apiRequest('/admin/system/stats'); // Update CPU this.updateProgressCircle('cpu-progress', response.cpu.usage_percent); document.getElementById('cpu-text').textContent = `${response.cpu.usage_percent}%`; document.getElementById('cpu-cores').textContent = `${response.cpu.core_count} cores`; // Update Memory this.updateProgressCircle('memory-progress', response.memory.usage_percent); document.getElementById('memory-text').textContent = `${response.memory.usage_percent}%`; document.getElementById('memory-details').textContent = `${response.memory.used_gb} / ${response.memory.total_gb} GB`; // Update Disk this.updateProgressCircle('disk-progress', response.disk.usage_percent); document.getElementById('disk-text').textContent = `${response.disk.usage_percent}%`; document.getElementById('disk-details').textContent = `${response.disk.used_gb} / ${response.disk.total_gb} GB`; // Update GPU if (response.gpu && response.gpu.length > 0) { const gpu = response.gpu[0]; this.updateProgressCircle('gpu-progress', gpu.load); document.getElementById('gpu-text').textContent = `${gpu.load}%`; document.getElementById('gpu-details').textContent = `${gpu.name} - ${gpu.temperature}°C`; } else { document.getElementById('gpu-text').textContent = '--'; document.getElementById('gpu-details').textContent = 'No GPU detected'; } } catch (error) { console.error('Failed to load system stats:', error); } } updateProgressCircle(elementId, percentage) { const element = document.getElementById(elementId); const degrees = (percentage / 100) * 360; // Remove existing color classes element.classList.remove('low', 'medium', 'high'); // Add appropriate color class if (percentage < 50) { element.classList.add('low'); } else if (percentage < 80) { element.classList.add('medium'); } else { element.classList.add('high'); } // Update CSS custom property for progress element.style.setProperty('--progress', `${degrees}deg`); } // Phase 2: Model Download async openModelDownload() { try { const response = await this.apiRequest('/admin/models/available'); const models = response.available_models || []; const container = document.getElementById('available-models-list'); if (models.length === 0) { container.innerHTML = '
No models available
'; } else { container.innerHTML = models.map(model => `
${model.name}
${model.description}
${model.tags.map(tag => `${tag}`).join('')}
Size: ${model.size}
`).join(''); } this.openModal('model-download-modal'); } catch (error) { console.error('Failed to load available models:', error); alert('Failed to load available models'); } } async downloadModel(modelName) { try { const response = await this.apiRequest('/admin/models/download', { method: 'POST', body: JSON.stringify({ model: modelName }) }); if (response.success) { alert(`Download started: ${response.message}`); this.closeModal('model-download-modal'); // Refresh models list after a short delay setTimeout(() => this.loadModels(), 2000); } else { alert(`Download failed: ${response.error}`); } } catch (error) { alert(`Download failed: ${error.message}`); } } // Phase 2: Model Delete confirmDeleteModel(modelName) { document.getElementById('delete-model-name').textContent = modelName; // Set up delete confirmation const confirmBtn = document.getElementById('confirm-delete-btn'); confirmBtn.onclick = () => this.deleteModel(modelName); this.openModal('model-delete-modal'); } async deleteModel(modelName) { try { const response = await this.apiRequest(`/admin/models/${modelName}`, { method: 'DELETE' }); if (response.success) { alert(`Model deleted: ${response.message}`); this.closeModal('model-delete-modal'); await this.loadModels(); } else { alert(`Delete failed: ${response.error}`); } } catch (error) { alert(`Delete failed: ${error.message}`); } } // Modal management openModal(modalId) { document.getElementById(modalId).style.display = 'block'; } closeModal(modalId) { document.getElementById(modalId).style.display = 'none'; } } // Global functions for HTML onclick handlers let admin; function refreshModels() { admin.refreshModels(); } function generateApiKey() { admin.generateApiKey(); } function openModelDownload() { admin.openModelDownload(); } function closeModal(modalId) { admin.closeModal(modalId); } // Phase 3: Logout function async function logout() { if (!confirm('Are you sure you want to logout?')) return; try { // Call logout API await admin.apiRequest('/admin/logout', { method: 'POST' }); } catch (error) { console.error('Logout API call failed:', error); } finally { // Clear local storage and redirect localStorage.removeItem('ai_admin_token'); localStorage.removeItem('ai_admin_user'); window.location.href = '/login'; } } // Initialize dashboard when page loads document.addEventListener('DOMContentLoaded', () => { admin = new AdminDashboard(); });