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 -

- - - +
+ - + + + @@ -127,88 +121,48 @@ {% if transport_disruptions and transport_disruptions|length > 0 %} - + - - - - - - {% endif %} - - - - - - {% for section in category_sections %} + -
+ +

+ ⚡️ 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 %}
-
-
-

- 🚆 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.

-
-
- +
+ - @@ -216,155 +170,99 @@ - {% for article in section.articles %} - - - - - {% if not loop.last %} - - - - {% endif %} - {% endfor %} - - - {% if not loop.last %} - - - - {% endif %} - {% endfor %} - - - - - - - - + + + {% endfor %} + {% endfor %} + - + + + +
-

- {{ section.icon }} {{ section.name }} -

-

- Top stories in {{ section.name.lower() }} +

+

+ {{ section.icon }}   {{ section.name }}

- - - - - -
- - {{ loop.index }} - -
+
+

+ {% if article.is_clustered %} + Multiple Sources + {% else %} + {{ article.source }} + {% endif %} +

- -

- {{ article.title_en if article.title_en else article.title }} -

+

+ + {{ 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: -

-
- {% for source in article.sources %} - - {{ source.name }} → - - {% endfor %} -
- {% else %} - - - Read more → - - {% endif %} -
-
-
-
-
-
-
- +
-
-

- 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 %} + + + + +
+ + Read full story → + +
+ {% endif %}
-

- 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.

-
- + - + \ No newline at end of file