// Admin Dashboard JavaScript
// Use relative URL to go through frontend proxy
const API_BASE = window.location.origin;
// Load all data on page load
document.addEventListener('DOMContentLoaded', () => {
loadSystemStats();
loadOllamaStatus();
loadGPUStatus();
loadPerformanceTest();
loadModels();
loadConfig();
loadClusteringStats();
});
// Refresh all data
function refreshAll() {
loadSystemStats();
loadOllamaStatus();
loadGPUStatus();
loadPerformanceTest();
loadModels();
loadConfig();
loadClusteringStats();
}
// Load system statistics
async function loadSystemStats() {
try {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 10000); // 10 second timeout
const response = await fetch(`${API_BASE}/api/stats`, { signal: controller.signal });
clearTimeout(timeoutId);
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const data = await response.json();
const html = `
Total Articles
${data.articles || 0}
Crawled Articles
${data.crawled_articles || 0}
AI Summarized
${data.summarized_articles || 0}
Clustered Articles
${data.clustered_articles || 0}
Neutral Summaries
${data.neutral_summaries || 0}
Active Subscribers
${data.subscribers || 0}
`;
document.getElementById('systemStats').innerHTML = html;
} catch (error) {
console.error('Error loading system stats:', error);
const errorMsg = error.name === 'AbortError' ? 'Request timeout' : error.message;
document.getElementById('systemStats').innerHTML = `Error: ${errorMsg}
`;
}
}
// Load Ollama status
async function loadOllamaStatus() {
try {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 10000);
const response = await fetch(`${API_BASE}/api/ollama/ping`, { signal: controller.signal });
clearTimeout(timeoutId);
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
const data = await response.json();
const isActive = data.status === 'success';
const statusClass = isActive ? 'status-active' : 'status-inactive';
const html = `
Status
${isActive ? 'Active' : 'Inactive'}
Base URL
${data.ollama_config?.base_url || 'N/A'}
Current Model
${data.ollama_config?.model || 'N/A'}
Enabled
${data.ollama_config?.enabled ? 'Yes' : 'No'}
${isActive ? `
Response
${data.response?.substring(0, 50)}...
` : ''}
`;
document.getElementById('ollamaStatus').innerHTML = html;
} catch (error) {
console.error('Error loading Ollama status:', error);
const errorMsg = error.name === 'AbortError' ? 'Request timeout' : error.message;
document.getElementById('ollamaStatus').innerHTML = `Error: ${errorMsg}
`;
}
}
// Load GPU status
async function loadGPUStatus() {
try {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 10000);
const response = await fetch(`${API_BASE}/api/ollama/gpu-status`, { signal: controller.signal });
clearTimeout(timeoutId);
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
const data = await response.json();
const gpuActive = data.gpu_in_use || false;
const gpuAvailable = data.gpu_available || false;
const statusClass = gpuActive ? 'status-active' : 'status-warning';
let html = `
GPU Available
${gpuAvailable ? 'Yes' : 'No'}
GPU In Use
${gpuActive ? 'Yes' : 'No (CPU Mode)'}
Models Loaded
${data.models_loaded || 0}
`;
// Add GPU details if available
if (data.gpu_details) {
const details = data.gpu_details;
if (details.model || details.gpu_name) {
html += `
GPU Model
${details.model || details.gpu_name || 'N/A'}
`;
}
if (details.gpu_layers !== undefined) {
html += `
GPU Layers
${details.gpu_layers}
`;
}
if (details.layers_offloaded) {
html += `
Layers Offloaded
${details.layers_offloaded}
`;
}
if (details.memory_used) {
html += `
GPU Memory
${details.memory_used}
`;
}
if (details.utilization) {
html += `
GPU Utilization
${details.utilization}
`;
}
if (details.note) {
html += `
âšī¸ ${details.note}
`;
}
}
// Add recommendation
if (data.recommendation) {
const bgColor = gpuActive ? '#d1fae5' : '#fef3c7';
const icon = gpuActive ? 'â' : 'đĄ';
html += `
${icon} ${data.recommendation}
`;
}
document.getElementById('gpuStatus').innerHTML = html;
} catch (error) {
console.error('Error loading GPU status:', error);
const errorMsg = error.name === 'AbortError' ? 'Request timeout' : error.message;
document.getElementById('gpuStatus').innerHTML = `Error: ${errorMsg}
`;
}
}
// Load performance test
async function loadPerformanceTest() {
document.getElementById('performanceTest').innerHTML = 'Click "Run Test" to check performance
';
}
// Run performance test
async function runPerformanceTest() {
document.getElementById('performanceTest').innerHTML = 'Running test...
';
try {
const response = await fetch(`${API_BASE}/api/ollama/test`);
const data = await response.json();
let badgeClass = 'badge-fair';
if (data.duration_seconds < 5) badgeClass = 'badge-excellent';
else if (data.duration_seconds < 15) badgeClass = 'badge-good';
else if (data.duration_seconds > 30) badgeClass = 'badge-slow';
const html = `
Duration
${data.duration_seconds}s
Performance
${data.performance}
Model
${data.model}
${data.recommendation}
`;
document.getElementById('performanceTest').innerHTML = html;
} catch (error) {
document.getElementById('performanceTest').innerHTML = `Error: ${error.message}
`;
}
}
// Load available models
async function loadModels() {
try {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 10000);
const response = await fetch(`${API_BASE}/api/ollama/models`, { signal: controller.signal });
clearTimeout(timeoutId);
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
const data = await response.json();
if (data.models && data.models.length > 0) {
const modelsList = data.models.map(model => {
const isCurrent = model === data.current_model;
return `${model} ${isCurrent ? '(current)' : ''}`;
}).join('');
const html = `
Current: ${data.current_model}
`;
document.getElementById('modelsList').innerHTML = html;
} else {
document.getElementById('modelsList').innerHTML = 'No models found
';
}
} catch (error) {
console.error('Error loading models:', error);
const errorMsg = error.name === 'AbortError' ? 'Request timeout' : error.message;
document.getElementById('modelsList').innerHTML = `Error: ${errorMsg}
`;
}
}
// Load configuration
async function loadConfig() {
try {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 10000);
const response = await fetch(`${API_BASE}/api/ollama/config`, { signal: controller.signal });
clearTimeout(timeoutId);
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
const data = await response.json();
const html = `
Base URL
${data.ollama_config?.base_url || 'N/A'}
Model
${data.ollama_config?.model || 'N/A'}
Enabled
${data.ollama_config?.enabled ? 'Yes' : 'No'}
Has API Key
${data.ollama_config?.has_api_key ? 'Yes' : 'No'}
Config file: ${data.env_file_path || 'N/A'}
Exists: ${data.env_file_exists ? 'Yes' : 'No'}
`;
document.getElementById('configInfo').innerHTML = html;
} catch (error) {
console.error('Error loading config:', error);
const errorMsg = error.name === 'AbortError' ? 'Request timeout' : error.message;
document.getElementById('configInfo').innerHTML = `Error: ${errorMsg}
`;
}
}
// Load clustering statistics
async function loadClusteringStats() {
try {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 10000);
const response = await fetch(`${API_BASE}/api/stats`, { signal: controller.signal });
clearTimeout(timeoutId);
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
const data = await response.json();
const clusteringRate = data.clustered_articles > 0
? ((data.neutral_summaries / data.clustered_articles) * 100).toFixed(1)
: 0;
const html = `
Total Articles
${data.articles || 0}
Clustered Articles
${data.clustered_articles || 0}
Neutral Summaries
${data.neutral_summaries || 0}
Clustering Rate
${clusteringRate}%
Multi-Source Stories
${data.neutral_summaries || 0}
AI Clustering: Automatically detects duplicate stories from different sources and generates neutral summaries.
`;
document.getElementById('clusteringStats').innerHTML = html;
} catch (error) {
console.error('Error loading clustering stats:', error);
const errorMsg = error.name === 'AbortError' ? 'Request timeout' : error.message;
document.getElementById('clusteringStats').innerHTML = `Error: ${errorMsg}
`;
}
}
// Subscriber Management
async function viewSubscribers() {
const listDiv = document.getElementById('subscriberList');
const messageDiv = document.getElementById('subscriberMessage');
listDiv.innerHTML = 'Loading subscribers...
';
messageDiv.innerHTML = '';
try {
const response = await fetch('/api/subscribers');
const data = await response.json();
if (data.subscribers && data.subscribers.length > 0) {
let html = '';
html += '
';
html += '| Email | Categories | Status |
';
html += '';
data.subscribers.forEach(sub => {
const categories = sub.categories ? sub.categories.join(', ') : 'All';
html += `
| ${sub.email} |
${categories} |
${sub.status} |
`;
});
html += '
';
html += `Total: ${data.total} subscribers
`;
listDiv.innerHTML = html;
} else {
listDiv.innerHTML = 'No subscribers found.
';
}
} catch (error) {
listDiv.innerHTML = 'Failed to load subscribers
';
}
}
async function deleteAllSubscribers() {
const messageDiv = document.getElementById('subscriberMessage');
if (!confirm('â ī¸ Are you sure you want to delete ALL subscribers? This cannot be undone!')) {
return;
}
if (!confirm('â ī¸ FINAL WARNING: This will permanently delete all subscriber data. Continue?')) {
return;
}
messageDiv.innerHTML = 'Deleting all subscribers...
';
try {
const response = await fetch('/api/admin/subscribers/delete-all', {
method: 'DELETE'
});
const data = await response.json();
if (response.ok) {
messageDiv.innerHTML = `â ${data.message}
`;
document.getElementById('subscriberList').innerHTML = '';
// Refresh stats
loadStats();
} else {
messageDiv.innerHTML = `â ${data.error || 'Failed to delete subscribers'}
`;
}
} catch (error) {
messageDiv.innerHTML = 'â Network error
';
}
}
// RSS Feed Management
async function exportRSSFeeds() {
const messageDiv = document.getElementById('rssFeedMessage');
messageDiv.innerHTML = 'Exporting RSS feeds...
';
try {
const response = await fetch('/api/rss-feeds/export');
const data = await response.json();
if (response.ok) {
// Create download
const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `rss-feeds-${new Date().toISOString().split('T')[0]}.json`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
messageDiv.innerHTML = `â Exported ${data.total} RSS feeds
`;
} else {
messageDiv.innerHTML = `â ${data.error || 'Export failed'}
`;
}
} catch (error) {
messageDiv.innerHTML = 'â Network error
';
}
}
async function importRSSFeeds(event) {
const messageDiv = document.getElementById('rssFeedMessage');
const file = event.target.files[0];
if (!file) return;
messageDiv.innerHTML = 'Importing RSS feeds...
';
try {
const text = await file.text();
const data = JSON.parse(text);
const response = await fetch('/api/rss-feeds/import', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(data)
});
const result = await response.json();
if (response.ok) {
let html = `â ${result.message}
`;
if (result.errors && result.errors.length > 0) {
html += 'Show details
';
result.errors.forEach(err => {
html += `- ${err}
`;
});
html += '
';
}
messageDiv.innerHTML = html;
// Refresh feed list if visible
if (document.getElementById('rssFeedList').innerHTML) {
viewRSSFeeds();
}
} else {
messageDiv.innerHTML = `â ${result.error || 'Import failed'}
`;
}
} catch (error) {
messageDiv.innerHTML = `â Error: ${error.message}
`;
}
// Reset file input
event.target.value = '';
}
async function viewRSSFeeds() {
const listDiv = document.getElementById('rssFeedList');
const messageDiv = document.getElementById('rssFeedMessage');
listDiv.innerHTML = 'Loading RSS feeds...
';
messageDiv.innerHTML = '';
try {
const response = await fetch('/api/rss-feeds');
const data = await response.json();
if (data.feeds && data.feeds.length > 0) {
let html = '';
html += '
';
html += '| Name | Category | URL | Status |
';
html += '';
data.feeds.forEach(feed => {
const statusColor = feed.active ? 'green' : 'gray';
const statusText = feed.active ? 'Active' : 'Inactive';
html += `
| ${feed.name} |
${feed.category} |
${feed.url} |
${statusText} |
`;
});
html += '
';
html += `Total: ${data.total} feeds
`;
listDiv.innerHTML = html;
} else {
listDiv.innerHTML = 'No RSS feeds found.
';
}
} catch (error) {
listDiv.innerHTML = 'Failed to load RSS feeds
';
}
}
// Recent Summarization Activity
let autoRefreshInterval = null;
async function loadRecentArticles() {
const container = document.getElementById('recentArticles');
try {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 10000);
const response = await fetch('/api/admin/recent-articles', { signal: controller.signal });
clearTimeout(timeoutId);
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
const data = await response.json();
if (data.articles && data.articles.length > 0) {
let html = '';
html += '
';
html += '| Time | Title | Source | Category | Words |
';
html += '';
data.articles.forEach(article => {
const time = article.summarized_at ? new Date(article.summarized_at).toLocaleTimeString() : 'N/A';
const title = article.title_en || article.title;
const categoryColors = {
'general': '#667eea',
'local': '#f59e0b',
'sports': '#10b981',
'science': '#8b5cf6'
};
const categoryColor = categoryColors[article.category] || '#6b7280';
html += `
| ${time} |
${title} |
${article.source} |
${article.category || 'N/A'} |
${article.summary_word_count || 'N/A'} |
`;
});
html += '
';
html += `Last updated: ${new Date().toLocaleTimeString()}
`;
container.innerHTML = html;
} else {
container.innerHTML = 'No summarized articles found.
';
}
} catch (error) {
console.error('Error loading recent articles:', error);
const errorMsg = error.name === 'AbortError' ? 'Request timeout' : error.message;
container.innerHTML = `Error: ${errorMsg}
`;
}
}
function toggleAutoRefresh() {
const checkbox = document.getElementById('autoRefresh');
if (checkbox.checked) {
// Start auto-refresh every 10 seconds
loadRecentArticles();
autoRefreshInterval = setInterval(loadRecentArticles, 10000);
} else {
// Stop auto-refresh
if (autoRefreshInterval) {
clearInterval(autoRefreshInterval);
autoRefreshInterval = null;
}
}
}
// Load recent articles on page load
document.addEventListener('DOMContentLoaded', () => {
loadRecentArticles();
});