Files
Munich-news/frontend/public/admin.js
2025-11-20 12:38:29 +01:00

700 lines
28 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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 = `
<div class="stat-row">
<span class="stat-label">Total Articles</span>
<span class="stat-value">${data.articles || 0}</span>
</div>
<div class="stat-row">
<span class="stat-label">Crawled Articles</span>
<span class="stat-value">${data.crawled_articles || 0}</span>
</div>
<div class="stat-row">
<span class="stat-label">AI Summarized</span>
<span class="stat-value">${data.summarized_articles || 0}</span>
</div>
<div class="stat-row">
<span class="stat-label">Clustered Articles</span>
<span class="stat-value">${data.clustered_articles || 0}</span>
</div>
<div class="stat-row">
<span class="stat-label">Neutral Summaries</span>
<span class="stat-value">${data.neutral_summaries || 0}</span>
</div>
<div class="stat-row">
<span class="stat-label">Active Subscribers</span>
<span class="stat-value">${data.subscribers || 0}</span>
</div>
`;
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 = `<div class="error">Error: ${errorMsg}</div>`;
}
}
// 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 = `
<div class="stat-row">
<span class="stat-label">Status</span>
<span class="stat-value">
<span class="status-indicator ${statusClass}"></span>
${isActive ? 'Active' : 'Inactive'}
</span>
</div>
<div class="stat-row">
<span class="stat-label">Base URL</span>
<span class="stat-value">${data.ollama_config?.base_url || 'N/A'}</span>
</div>
<div class="stat-row">
<span class="stat-label">Current Model</span>
<span class="stat-value">${data.ollama_config?.model || 'N/A'}</span>
</div>
<div class="stat-row">
<span class="stat-label">Enabled</span>
<span class="stat-value">${data.ollama_config?.enabled ? 'Yes' : 'No'}</span>
</div>
${isActive ? `
<div class="stat-row">
<span class="stat-label">Response</span>
<span class="stat-value" style="font-size: 12px;">${data.response?.substring(0, 50)}...</span>
</div>
` : ''}
`;
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 = `<div class="error">Error: ${errorMsg}</div>`;
}
}
// 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 = `
<div class="stat-row">
<span class="stat-label">GPU Available</span>
<span class="stat-value">
<span class="status-indicator ${gpuAvailable ? 'status-active' : 'status-inactive'}"></span>
${gpuAvailable ? 'Yes' : 'No'}
</span>
</div>
<div class="stat-row">
<span class="stat-label">GPU In Use</span>
<span class="stat-value">
<span class="status-indicator ${statusClass}"></span>
${gpuActive ? 'Yes' : 'No (CPU Mode)'}
</span>
</div>
<div class="stat-row">
<span class="stat-label">Models Loaded</span>
<span class="stat-value">${data.models_loaded || 0}</span>
</div>
`;
// Add GPU details if available
if (data.gpu_details) {
const details = data.gpu_details;
if (details.model || details.gpu_name) {
html += `
<div class="stat-row">
<span class="stat-label">GPU Model</span>
<span class="stat-value" style="font-size: 12px;">${details.model || details.gpu_name || 'N/A'}</span>
</div>`;
}
if (details.gpu_layers !== undefined) {
html += `
<div class="stat-row">
<span class="stat-label">GPU Layers</span>
<span class="stat-value">${details.gpu_layers}</span>
</div>`;
}
if (details.layers_offloaded) {
html += `
<div class="stat-row">
<span class="stat-label">Layers Offloaded</span>
<span class="stat-value">${details.layers_offloaded}</span>
</div>`;
}
if (details.memory_used) {
html += `
<div class="stat-row">
<span class="stat-label">GPU Memory</span>
<span class="stat-value">${details.memory_used}</span>
</div>`;
}
if (details.utilization) {
html += `
<div class="stat-row">
<span class="stat-label">GPU Utilization</span>
<span class="stat-value">${details.utilization}</span>
</div>`;
}
if (details.note) {
html += `
<div style="margin-top: 10px; padding: 10px; background: #dbeafe; border-radius: 5px; font-size: 11px;">
${details.note}
</div>`;
}
}
// Add recommendation
if (data.recommendation) {
const bgColor = gpuActive ? '#d1fae5' : '#fef3c7';
const icon = gpuActive ? '✓' : '💡';
html += `
<div style="margin-top: 10px; padding: 10px; background: ${bgColor}; border-radius: 5px; font-size: 12px; white-space: pre-line;">
${icon} ${data.recommendation}
</div>`;
}
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 = `<div class="error">Error: ${errorMsg}</div>`;
}
}
// Load performance test
async function loadPerformanceTest() {
document.getElementById('performanceTest').innerHTML = '<div class="loading">Click "Run Test" to check performance</div>';
}
// Run performance test
async function runPerformanceTest() {
document.getElementById('performanceTest').innerHTML = '<div class="loading">Running test...</div>';
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 = `
<div class="stat-row">
<span class="stat-label">Duration</span>
<span class="stat-value">${data.duration_seconds}s</span>
</div>
<div class="stat-row">
<span class="stat-label">Performance</span>
<span class="stat-value">
<span class="performance-badge ${badgeClass}">${data.performance}</span>
</span>
</div>
<div class="stat-row">
<span class="stat-label">Model</span>
<span class="stat-value">${data.model}</span>
</div>
<div style="margin-top: 10px; padding: 10px; background: #f3f4f6; border-radius: 5px; font-size: 12px;">
${data.recommendation}
</div>
`;
document.getElementById('performanceTest').innerHTML = html;
} catch (error) {
document.getElementById('performanceTest').innerHTML = `<div class="error">Error: ${error.message}</div>`;
}
}
// 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 `<li class="${isCurrent ? 'current-model' : ''}">${model} ${isCurrent ? '(current)' : ''}</li>`;
}).join('');
const html = `
<ul class="model-list">
${modelsList}
</ul>
<div style="margin-top: 10px; font-size: 12px; color: #666;">
Current: ${data.current_model}
</div>
`;
document.getElementById('modelsList').innerHTML = html;
} else {
document.getElementById('modelsList').innerHTML = '<div>No models found</div>';
}
} catch (error) {
console.error('Error loading models:', error);
const errorMsg = error.name === 'AbortError' ? 'Request timeout' : error.message;
document.getElementById('modelsList').innerHTML = `<div class="error">Error: ${errorMsg}</div>`;
}
}
// 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 = `
<div class="stat-row">
<span class="stat-label">Base URL</span>
<span class="stat-value" style="font-size: 12px;">${data.ollama_config?.base_url || 'N/A'}</span>
</div>
<div class="stat-row">
<span class="stat-label">Model</span>
<span class="stat-value">${data.ollama_config?.model || 'N/A'}</span>
</div>
<div class="stat-row">
<span class="stat-label">Enabled</span>
<span class="stat-value">${data.ollama_config?.enabled ? 'Yes' : 'No'}</span>
</div>
<div class="stat-row">
<span class="stat-label">Has API Key</span>
<span class="stat-value">${data.ollama_config?.has_api_key ? 'Yes' : 'No'}</span>
</div>
<div style="margin-top: 10px; padding: 10px; background: #f3f4f6; border-radius: 5px; font-size: 11px;">
<strong>Config file:</strong> ${data.env_file_path || 'N/A'}<br>
<strong>Exists:</strong> ${data.env_file_exists ? 'Yes' : 'No'}
</div>
`;
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 = `<div class="error">Error: ${errorMsg}</div>`;
}
}
// 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 = `
<div class="dashboard-grid">
<div>
<div class="stat-row">
<span class="stat-label">Total Articles</span>
<span class="stat-value">${data.articles || 0}</span>
</div>
<div class="stat-row">
<span class="stat-label">Clustered Articles</span>
<span class="stat-value">${data.clustered_articles || 0}</span>
</div>
<div class="stat-row">
<span class="stat-label">Neutral Summaries</span>
<span class="stat-value">${data.neutral_summaries || 0}</span>
</div>
</div>
<div>
<div class="stat-row">
<span class="stat-label">Clustering Rate</span>
<span class="stat-value">${clusteringRate}%</span>
</div>
<div class="stat-row">
<span class="stat-label">Multi-Source Stories</span>
<span class="stat-value">${data.neutral_summaries || 0}</span>
</div>
<div style="margin-top: 10px; padding: 10px; background: #dbeafe; border-radius: 5px; font-size: 12px;">
<strong>AI Clustering:</strong> Automatically detects duplicate stories from different sources and generates neutral summaries.
</div>
</div>
</div>
`;
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 = `<div class="error">Error: ${errorMsg}</div>`;
}
}
// Subscriber Management
async function viewSubscribers() {
const listDiv = document.getElementById('subscriberList');
const messageDiv = document.getElementById('subscriberMessage');
listDiv.innerHTML = '<p class="loading">Loading subscribers...</p>';
messageDiv.innerHTML = '';
try {
const response = await fetch('/api/subscribers');
const data = await response.json();
if (data.subscribers && data.subscribers.length > 0) {
let html = '<div style="max-height: 400px; overflow-y: auto; margin-top: 15px;">';
html += '<table style="width: 100%; border-collapse: collapse;">';
html += '<thead><tr style="background: #f5f5f5; position: sticky; top: 0;"><th style="padding: 10px; text-align: left;">Email</th><th style="padding: 10px; text-align: left;">Categories</th><th style="padding: 10px; text-align: left;">Status</th></tr></thead>';
html += '<tbody>';
data.subscribers.forEach(sub => {
const categories = sub.categories ? sub.categories.join(', ') : 'All';
html += `<tr style="border-bottom: 1px solid #eee;">
<td style="padding: 10px;">${sub.email}</td>
<td style="padding: 10px;">${categories}</td>
<td style="padding: 10px;"><span style="color: ${sub.status === 'active' ? 'green' : 'red'};">${sub.status}</span></td>
</tr>`;
});
html += '</tbody></table></div>';
html += `<p style="margin-top: 10px; color: #666;">Total: ${data.total} subscribers</p>`;
listDiv.innerHTML = html;
} else {
listDiv.innerHTML = '<p style="color: #666;">No subscribers found.</p>';
}
} catch (error) {
listDiv.innerHTML = '<p style="color: red;">Failed to load subscribers</p>';
}
}
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 = '<p class="loading">Deleting all subscribers...</p>';
try {
const response = await fetch('/api/admin/subscribers/delete-all', {
method: 'DELETE'
});
const data = await response.json();
if (response.ok) {
messageDiv.innerHTML = `<p style="color: green;">✓ ${data.message}</p>`;
document.getElementById('subscriberList').innerHTML = '';
// Refresh stats
loadStats();
} else {
messageDiv.innerHTML = `<p style="color: red;">✗ ${data.error || 'Failed to delete subscribers'}</p>`;
}
} catch (error) {
messageDiv.innerHTML = '<p style="color: red;">✗ Network error</p>';
}
}
// RSS Feed Management
async function exportRSSFeeds() {
const messageDiv = document.getElementById('rssFeedMessage');
messageDiv.innerHTML = '<p class="loading">Exporting RSS feeds...</p>';
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 = `<p style="color: green;">✓ Exported ${data.total} RSS feeds</p>`;
} else {
messageDiv.innerHTML = `<p style="color: red;">✗ ${data.error || 'Export failed'}</p>`;
}
} catch (error) {
messageDiv.innerHTML = '<p style="color: red;">✗ Network error</p>';
}
}
async function importRSSFeeds(event) {
const messageDiv = document.getElementById('rssFeedMessage');
const file = event.target.files[0];
if (!file) return;
messageDiv.innerHTML = '<p class="loading">Importing RSS feeds...</p>';
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 = `<p style="color: green;">✓ ${result.message}</p>`;
if (result.errors && result.errors.length > 0) {
html += '<details style="margin-top: 10px;"><summary>Show details</summary><ul>';
result.errors.forEach(err => {
html += `<li style="color: orange;">${err}</li>`;
});
html += '</ul></details>';
}
messageDiv.innerHTML = html;
// Refresh feed list if visible
if (document.getElementById('rssFeedList').innerHTML) {
viewRSSFeeds();
}
} else {
messageDiv.innerHTML = `<p style="color: red;">✗ ${result.error || 'Import failed'}</p>`;
}
} catch (error) {
messageDiv.innerHTML = `<p style="color: red;">✗ Error: ${error.message}</p>`;
}
// Reset file input
event.target.value = '';
}
async function viewRSSFeeds() {
const listDiv = document.getElementById('rssFeedList');
const messageDiv = document.getElementById('rssFeedMessage');
listDiv.innerHTML = '<p class="loading">Loading RSS feeds...</p>';
messageDiv.innerHTML = '';
try {
const response = await fetch('/api/rss-feeds');
const data = await response.json();
if (data.feeds && data.feeds.length > 0) {
let html = '<div style="max-height: 400px; overflow-y: auto; margin-top: 15px;">';
html += '<table style="width: 100%; border-collapse: collapse;">';
html += '<thead><tr style="background: #f5f5f5; position: sticky; top: 0;"><th style="padding: 10px; text-align: left;">Name</th><th style="padding: 10px; text-align: left;">Category</th><th style="padding: 10px; text-align: left;">URL</th><th style="padding: 10px; text-align: left;">Status</th></tr></thead>';
html += '<tbody>';
data.feeds.forEach(feed => {
const statusColor = feed.active ? 'green' : 'gray';
const statusText = feed.active ? 'Active' : 'Inactive';
html += `<tr style="border-bottom: 1px solid #eee;">
<td style="padding: 10px;">${feed.name}</td>
<td style="padding: 10px;">${feed.category}</td>
<td style="padding: 10px; font-size: 12px; max-width: 300px; overflow: hidden; text-overflow: ellipsis;">${feed.url}</td>
<td style="padding: 10px;"><span style="color: ${statusColor};">${statusText}</span></td>
</tr>`;
});
html += '</tbody></table></div>';
html += `<p style="margin-top: 10px; color: #666;">Total: ${data.total} feeds</p>`;
listDiv.innerHTML = html;
} else {
listDiv.innerHTML = '<p style="color: #666;">No RSS feeds found.</p>';
}
} catch (error) {
listDiv.innerHTML = '<p style="color: red;">Failed to load RSS feeds</p>';
}
}
// 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 = '<div style="max-height: 500px; overflow-y: auto;">';
html += '<table style="width: 100%; border-collapse: collapse; font-size: 14px;">';
html += '<thead><tr style="background: #f5f5f5; position: sticky; top: 0;"><th style="padding: 8px; text-align: left;">Time</th><th style="padding: 8px; text-align: left;">Title</th><th style="padding: 8px; text-align: left;">Source</th><th style="padding: 8px; text-align: left;">Category</th><th style="padding: 8px; text-align: right;">Words</th></tr></thead>';
html += '<tbody>';
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 += `<tr style="border-bottom: 1px solid #eee;">
<td style="padding: 8px; white-space: nowrap; color: #666;">${time}</td>
<td style="padding: 8px; max-width: 400px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;" title="${title}">${title}</td>
<td style="padding: 8px;">${article.source}</td>
<td style="padding: 8px;"><span style="background: ${categoryColor}; color: white; padding: 2px 8px; border-radius: 4px; font-size: 12px;">${article.category || 'N/A'}</span></td>
<td style="padding: 8px; text-align: right;">${article.summary_word_count || 'N/A'}</td>
</tr>`;
});
html += '</tbody></table></div>';
html += `<p style="margin-top: 10px; color: #666; font-size: 12px;">Last updated: ${new Date().toLocaleTimeString()}</p>`;
container.innerHTML = html;
} else {
container.innerHTML = '<p style="color: #666;">No summarized articles found.</p>';
}
} catch (error) {
console.error('Error loading recent articles:', error);
const errorMsg = error.name === 'AbortError' ? 'Request timeout' : error.message;
container.innerHTML = `<p style="color: red;">Error: ${errorMsg}</p>`;
}
}
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();
});