diff --git a/frontend/public/admin.js b/frontend/public/admin.js
index ca44752..891e712 100644
--- a/frontend/public/admin.js
+++ b/frontend/public/admin.js
@@ -27,7 +27,16 @@ function refreshAll() {
// Load system statistics
async function loadSystemStats() {
try {
- const response = await fetch(`${API_BASE}/api/stats`);
+ 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 = `
@@ -59,14 +68,25 @@ async function loadSystemStats() {
document.getElementById('systemStats').innerHTML = html;
} catch (error) {
- document.getElementById('systemStats').innerHTML = `
Error loading stats: ${error.message}
`;
+ 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 response = await fetch(`${API_BASE}/api/ollama/ping`);
+ 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';
@@ -102,25 +122,37 @@ async function loadOllamaStatus() {
document.getElementById('ollamaStatus').innerHTML = html;
} catch (error) {
- document.getElementById('ollamaStatus').innerHTML = `Error: ${error.message}
`;
+ 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 response = await fetch(`${API_BASE}/api/ollama/gpu-status`);
+ 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;
+ const gpuActive = data.gpu_in_use || false;
+ const gpuAvailable = data.gpu_available || false;
const statusClass = gpuActive ? 'status-active' : 'status-warning';
- const html = `
+ let html = `
GPU Available
-
- ${data.gpu_available ? 'Yes' : 'No'}
+
+ ${gpuAvailable ? 'Yes' : 'No'}
@@ -134,26 +166,76 @@ async function loadGPUStatus() {
Models Loaded
${data.models_loaded || 0}
- ${data.gpu_details ? `
-
- GPU Model
- ${data.gpu_details.model}
-
-
- GPU Layers
- ${data.gpu_details.gpu_layers}
-
- ` : ''}
- ${!gpuActive ? `
-
- 💡 Enable GPU for 5-10x faster processing
-
- ` : ''}
+ `;
+
+ // 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) {
- document.getElementById('gpuStatus').innerHTML = `Error: ${error.message}
`;
+ console.error('Error loading GPU status:', error);
+ const errorMsg = error.name === 'AbortError' ? 'Request timeout' : error.message;
+ document.getElementById('gpuStatus').innerHTML = `Error: ${errorMsg}
`;
}
}
@@ -204,7 +286,16 @@ async function runPerformanceTest() {
// Load available models
async function loadModels() {
try {
- const response = await fetch(`${API_BASE}/api/ollama/models`);
+ 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) {
@@ -227,14 +318,25 @@ async function loadModels() {
document.getElementById('modelsList').innerHTML = 'No models found
';
}
} catch (error) {
- document.getElementById('modelsList').innerHTML = `Error: ${error.message}
`;
+ 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 response = await fetch(`${API_BASE}/api/ollama/config`);
+ 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 = `
@@ -262,14 +364,25 @@ async function loadConfig() {
document.getElementById('configInfo').innerHTML = html;
} catch (error) {
- document.getElementById('configInfo').innerHTML = `Error: ${error.message}
`;
+ 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 response = await fetch(`${API_BASE}/api/stats`);
+ 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
@@ -310,7 +423,9 @@ async function loadClusteringStats() {
document.getElementById('clusteringStats').innerHTML = html;
} catch (error) {
- document.getElementById('clusteringStats').innerHTML = `Error: ${error.message}
`;
+ console.error('Error loading clustering stats:', error);
+ const errorMsg = error.name === 'AbortError' ? 'Request timeout' : error.message;
+ document.getElementById('clusteringStats').innerHTML = `Error: ${errorMsg}
`;
}
}
@@ -512,7 +627,16 @@ async function loadRecentArticles() {
const container = document.getElementById('recentArticles');
try {
- const response = await fetch('/api/admin/recent-articles');
+ 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) {
@@ -548,7 +672,9 @@ async function loadRecentArticles() {
container.innerHTML = 'No summarized articles found.
';
}
} catch (error) {
- container.innerHTML = 'Failed to load recent articles
';
+ console.error('Error loading recent articles:', error);
+ const errorMsg = error.name === 'AbortError' ? 'Request timeout' : error.message;
+ container.innerHTML = `Error: ${errorMsg}
`;
}
}
diff --git a/frontend/server.js b/frontend/server.js
index 1e99847..8261feb 100644
--- a/frontend/server.js
+++ b/frontend/server.js
@@ -70,6 +70,17 @@ app.post('/api/unsubscribe', async (req, res) => {
}
});
+app.get('/api/subscribers', async (req, res) => {
+ try {
+ const response = await axios.get(`${API_URL}/api/subscribers`);
+ res.json(response.data);
+ } catch (error) {
+ res.status(error.response?.status || 500).json(
+ error.response?.data || { error: 'Failed to get subscribers' }
+ );
+ }
+});
+
app.get('/api/subscribers/:email', async (req, res) => {
try {
const response = await axios.get(`${API_URL}/api/subscribers/${req.params.email}`);
diff --git a/news_sender/newsletter_template.html b/news_sender/newsletter_template.html
index f625818..530b4b1 100644
--- a/news_sender/newsletter_template.html
+++ b/news_sender/newsletter_template.html
@@ -5,67 +5,75 @@
Munich News Daily
-
-
-
-
+
+
+
-
-
-
+
+
+
-
-
-
- Munich News Daily
-
-
+ |
+
{{ date }}
+
+ Munich News Daily
+
|
-
- |
-
+ |
+
Good morning ☀️
-
- Here's what's happening in Munich today. We've summarized {{ article_count }} stories using AI so you can stay informed in under 5 minutes.
+
+ Here is your AI-curated briefing. We've summarized {{ article_count }} stories to get you up to speed in under 5 minutes.
-
+
{% if weather and weather.success %}
-
-
+
- |
+ |
- |
-
- TODAY'S WEATHER
-
-
- {{ weather.icon }} {{ weather.temperature }}°C
-
-
+ |
+
+ {{ weather.icon }} {{ weather.temperature }}°
+
+
{{ weather.condition }}
-
+
|
-
-
- High: {{ weather.high }}°C
-
-
- Low: {{ weather.low }}°C
-
+ |
+
+
+ |
+ High
+ {{ weather.high }}°
+ |
+
+ Low
+ {{ weather.low }}°
+ |
+
+
|
@@ -76,48 +84,34 @@
|
-
-
- |
-
- |
-
+ |
-
-
-
- 📋 TL;DR - Quick Summary
-
-
- Scan through today's top stories in seconds
-
-
-
-
+
+
- |
+ |
+
+ ⚡️ Quick Summary
+
+ |
+
+
+
{% for section in category_sections %}
{% for article in section.articles %}
-
+
- |
-
- {{ loop.index }}
-
+ |
+
|
-
- {{ article.title_en if article.title_en else article.title }}
-
- {{ article.summary.split('.')[0] }}.
+
+ {{ article.title_en if article.title_en else article.title }} — {{ article.summary.split('.')[0] }}.
|
- {% if not loop.last %}
-
- {% endif %}
{% endfor %}
{% endfor %}
@@ -127,88 +121,48 @@
{% if transport_disruptions and transport_disruptions|length > 0 %}
-
+ |
- |
-
- |
-
-
-
-
-
-
- 🚆 S-Bahn Disruptions Today
+
+
+ 🚆 S-Bahn Updates
-
- Current service disruptions affecting Munich S-Bahn:
-
{% for disruption in transport_disruptions %}
-
-
+
- |
-
-
- {{ disruption.severity_icon }} {{ disruption.lines_str }}
+ |
+
+ {{ disruption.severity_icon }} {{ disruption.lines_str }}
-
-
-
+
{{ disruption.title }}
-
-
{% if disruption.description %}
-
+
{{ disruption.description }}
{% endif %}
-
-
- {% if disruption.start_time_str or disruption.end_time_str %}
-
- ⏰
- {% if disruption.start_time_str %}
- From {{ disruption.start_time_str }}
- {% endif %}
- {% if disruption.end_time_str %}
- until {{ disruption.end_time_str }}
- {% endif %}
-
- {% endif %}
|
{% endfor %}
-
-
- 💡 Plan your commute accordingly. Check MVG.de for real-time updates.
+
+ Check MVG.de for live times.
{% endif %}
-
-
- |
-
- |
-
-
-
{% for section in category_sections %}
+
-
-
+
+
-
-
- {{ section.icon }} {{ section.name }}
-
-
- Top stories in {{ section.name.lower() }}
+ |
+
+ {{ section.icon }} {{ section.name }}
|
@@ -216,155 +170,99 @@
-
{% for article in section.articles %}
-
-
-
-
- |
-
- {{ loop.index }}
-
- |
-
-
+ |
+
+ {% if article.is_clustered %}
+ Multiple Sources
+ {% else %}
+ {{ article.source }}
+ {% endif %}
+
-
-
- {{ article.title_en if article.title_en else article.title }}
-
+
-
{% if article.title_en and article.title_en != article.title %}
-
- Original: {{ article.title }}
+
+ "{{ article.title }}"
{% endif %}
-
-
- {% if article.is_clustered %}
- Multiple sources
- {% else %}
- {{ article.source }}
- {% if article.author %}
- • {{ article.author }}
- {% endif %}
- {% endif %}
-
-
-
-
+
{{ article.summary }}
-
- {% if article.is_clustered and article.sources %}
-
-
- 📰 Covered by {{ article.article_count }} sources:
-
-
- {% else %}
-
-
- Read more →
-
- {% endif %}
- |
-
-
-
- {% if not loop.last %}
-
- |
-
- |
-
- {% endif %}
- {% endfor %}
-
-
- {% if not loop.last %}
-
- |
-
- |
-
- {% endif %}
- {% endfor %}
-
-
-
- |
-
- |
-
-
-
-
+
- |
-
- Today's Digest
-
-
- {{ article_count }}
-
-
- stories • AI-summarized • 5 min read
-
+ |
+ {% if article.is_clustered and article.sources %}
+
+ Read full coverage:
+ {% for source in article.sources %}
+
+ {{ source.name }} ↗
+
+ {% endfor %}
+
+ {% else %}
+
+ {% endif %}
|
-
+
+ |
+ {% endfor %}
+ {% endfor %}
+
- |
-
- Munich News Daily
+ |
+
+ {{ article_count }}
-
- AI-powered news summaries for busy people.
- Delivered daily to your inbox.
+
+ Stories Summarized
+
+ |
+
+
+
+ |
+
+ Munich News Daily
+ AI-powered news for busy locals.
-
-
- Visit Website
- •
- Manage Preferences
- •
- Unsubscribe
+
+ Website •
+ Preferences •
+ Unsubscribe
-
- {% if tracking_enabled %}
-
-
- This email contains tracking to measure engagement and improve our content.
- We respect your privacy and anonymize data after 90 days.
-
- {% endif %}
-
-
- © {{ year }} Munich News Daily. All rights reserved.
+
+
+ © {{ year }} Munich News Daily. Made with 🥨 in Bavaria.
|
-
|
-
+
-
+ | | | | | | | | | |