update
This commit is contained in:
@@ -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 = `<div class="error">Error loading stats: ${error.message}</div>`;
|
||||
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 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 = `<div class="error">Error: ${error.message}</div>`;
|
||||
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 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 = `
|
||||
<div class="stat-row">
|
||||
<span class="stat-label">GPU Available</span>
|
||||
<span class="stat-value">
|
||||
<span class="status-indicator ${data.gpu_available ? 'status-active' : 'status-inactive'}"></span>
|
||||
${data.gpu_available ? 'Yes' : 'No'}
|
||||
<span class="status-indicator ${gpuAvailable ? 'status-active' : 'status-inactive'}"></span>
|
||||
${gpuAvailable ? 'Yes' : 'No'}
|
||||
</span>
|
||||
</div>
|
||||
<div class="stat-row">
|
||||
@@ -134,26 +166,76 @@ async function loadGPUStatus() {
|
||||
<span class="stat-label">Models Loaded</span>
|
||||
<span class="stat-value">${data.models_loaded || 0}</span>
|
||||
</div>
|
||||
${data.gpu_details ? `
|
||||
<div class="stat-row">
|
||||
<span class="stat-label">GPU Model</span>
|
||||
<span class="stat-value">${data.gpu_details.model}</span>
|
||||
</div>
|
||||
<div class="stat-row">
|
||||
<span class="stat-label">GPU Layers</span>
|
||||
<span class="stat-value">${data.gpu_details.gpu_layers}</span>
|
||||
</div>
|
||||
` : ''}
|
||||
${!gpuActive ? `
|
||||
<div style="margin-top: 10px; padding: 10px; background: #fef3c7; border-radius: 5px; font-size: 12px;">
|
||||
💡 Enable GPU for 5-10x faster processing
|
||||
</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) {
|
||||
document.getElementById('gpuStatus').innerHTML = `<div class="error">Error: ${error.message}</div>`;
|
||||
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>`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 = '<div>No models found</div>';
|
||||
}
|
||||
} catch (error) {
|
||||
document.getElementById('modelsList').innerHTML = `<div class="error">Error: ${error.message}</div>`;
|
||||
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 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 = `<div class="error">Error: ${error.message}</div>`;
|
||||
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 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 = `<div class="error">Error: ${error.message}</div>`;
|
||||
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>`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 = '<p style="color: #666;">No summarized articles found.</p>';
|
||||
}
|
||||
} catch (error) {
|
||||
container.innerHTML = '<p style="color: red;">Failed to load recent articles</p>';
|
||||
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>`;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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}`);
|
||||
|
||||
@@ -5,67 +5,75 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<title>Munich News Daily</title>
|
||||
<!--[if mso]>
|
||||
<style type="text/css">
|
||||
body, table, td {font-family: Arial, Helvetica, sans-serif !important;}
|
||||
/* Client-specific resets */
|
||||
body, table, td, a { -webkit-text-size-adjust: 100%; -ms-text-size-adjust: 100%; }
|
||||
table, td { mso-table-lspace: 0pt; mso-table-rspace: 0pt; }
|
||||
img { -ms-interpolation-mode: bicubic; border: 0; height: auto; line-height: 100%; outline: none; text-decoration: none; }
|
||||
|
||||
/* General styles */
|
||||
body { height: 100% !important; margin: 0 !important; padding: 0 !important; width: 100% !important; }
|
||||
|
||||
/* Hover effects */
|
||||
.hover-opacity:hover { opacity: 0.8 !important; }
|
||||
.read-more-btn:hover { background-color: #556cd6 !important; }
|
||||
</style>
|
||||
<![endif]-->
|
||||
</head>
|
||||
<body style="margin: 0; padding: 0; background-color: #f4f4f4; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;">
|
||||
<!-- Wrapper Table -->
|
||||
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0" style="background-color: #f4f4f4;">
|
||||
<body style="margin: 0; padding: 0; background-color: #f0f2f5; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif; color: #1a1a1a;">
|
||||
|
||||
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0" style="background-color: #f0f2f5;">
|
||||
<tr>
|
||||
<td align="center" style="padding: 20px 0;">
|
||||
<!-- Main Container -->
|
||||
<table role="presentation" width="600" cellpadding="0" cellspacing="0" border="0" style="background-color: #ffffff; max-width: 600px;">
|
||||
<td align="center" style="padding: 30px 10px;">
|
||||
|
||||
<table role="presentation" width="600" cellpadding="0" cellspacing="0" border="0" style="max-width: 600px; width: 100%;">
|
||||
|
||||
<!-- Header -->
|
||||
<tr>
|
||||
<td style="background-color: #1a1a1a; padding: 30px 40px; text-align: center;">
|
||||
<h1 style="margin: 0 0 8px 0; font-size: 28px; font-weight: 700; color: #ffffff; letter-spacing: -0.5px;">
|
||||
Munich News Daily
|
||||
</h1>
|
||||
<p style="margin: 0; font-size: 14px; color: #999999; letter-spacing: 0.5px;">
|
||||
<td align="center" style="padding-bottom: 25px;">
|
||||
<p style="margin: 0 0 5px 0; font-size: 12px; font-weight: 700; letter-spacing: 1.5px; text-transform: uppercase; color: #667eea;">
|
||||
{{ date }}
|
||||
</p>
|
||||
<h1 style="margin: 0; font-size: 32px; font-weight: 800; letter-spacing: -1px; color: #1a1a1a;">
|
||||
Munich News Daily
|
||||
</h1>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- Greeting -->
|
||||
<tr>
|
||||
<td style="padding: 30px 40px 20px 40px;">
|
||||
<p style="margin: 0; font-size: 16px; line-height: 1.5; color: #333333;">
|
||||
<td style="background-color: #ffffff; border-radius: 16px; padding: 30px; margin-bottom: 20px; box-shadow: 0 2px 4px rgba(0,0,0,0.05);">
|
||||
<p style="margin: 0 0 10px 0; font-size: 18px; font-weight: 600; color: #1a1a1a;">
|
||||
Good morning ☀️
|
||||
</p>
|
||||
<p style="margin: 15px 0 0 0; font-size: 15px; line-height: 1.6; color: #666666; text-align: justify;">
|
||||
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.
|
||||
<p style="margin: 0 0 25px 0; font-size: 16px; line-height: 1.6; color: #555555;">
|
||||
Here is your AI-curated briefing. We've summarized <strong>{{ article_count }} stories</strong> to get you up to speed in under 5 minutes.
|
||||
</p>
|
||||
|
||||
|
||||
{% if weather and weather.success %}
|
||||
<!-- Weather Widget -->
|
||||
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0" style="margin-top: 20px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); border-radius: 12px; overflow: hidden;">
|
||||
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0" style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); border-radius: 12px; overflow: hidden;">
|
||||
<tr>
|
||||
<td style="padding: 20px;">
|
||||
<td style="padding: 20px 25px;">
|
||||
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0">
|
||||
<tr>
|
||||
<td style="width: 60%; vertical-align: middle;">
|
||||
<p style="margin: 0 0 5px 0; font-size: 13px; color: rgba(255,255,255,0.9); font-weight: 600;">
|
||||
TODAY'S WEATHER
|
||||
</p>
|
||||
<p style="margin: 0; font-size: 32px; color: #ffffff; font-weight: 700; line-height: 1;">
|
||||
{{ weather.icon }} {{ weather.temperature }}°C
|
||||
</p>
|
||||
<p style="margin: 5px 0 0 0; font-size: 14px; color: rgba(255,255,255,0.9);">
|
||||
<td style="vertical-align: middle;">
|
||||
<div style="font-size: 36px; font-weight: 700; color: #ffffff; line-height: 1;">
|
||||
{{ weather.icon }} {{ weather.temperature }}°
|
||||
</div>
|
||||
<div style="font-size: 14px; color: rgba(255,255,255,0.9); margin-top: 5px; font-weight: 500;">
|
||||
{{ weather.condition }}
|
||||
</p>
|
||||
</div>
|
||||
</td>
|
||||
<td style="width: 40%; text-align: right; vertical-align: middle;">
|
||||
<p style="margin: 0; font-size: 14px; color: rgba(255,255,255,0.9);">
|
||||
High: <strong style="color: #ffffff;">{{ weather.high }}°C</strong>
|
||||
</p>
|
||||
<p style="margin: 5px 0 0 0; font-size: 14px; color: rgba(255,255,255,0.9);">
|
||||
Low: <strong style="color: #ffffff;">{{ weather.low }}°C</strong>
|
||||
</p>
|
||||
<td align="right" style="vertical-align: middle;">
|
||||
<table role="presentation" cellpadding="0" cellspacing="0" border="0">
|
||||
<tr>
|
||||
<td style="padding-right: 15px; border-right: 1px solid rgba(255,255,255,0.3);">
|
||||
<div style="font-size: 12px; color: rgba(255,255,255,0.8); text-transform: uppercase;">High</div>
|
||||
<div style="font-size: 16px; font-weight: 700; color: #ffffff;">{{ weather.high }}°</div>
|
||||
</td>
|
||||
<td style="padding-left: 15px;">
|
||||
<div style="font-size: 12px; color: rgba(255,255,255,0.8); text-transform: uppercase;">Low</div>
|
||||
<div style="font-size: 16px; font-weight: 700; color: #ffffff;">{{ weather.low }}°</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
@@ -76,48 +84,34 @@
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- Divider -->
|
||||
<tr>
|
||||
<td style="padding: 0 40px;">
|
||||
<div style="height: 1px; background-color: #e0e0e0;"></div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr><td style="height: 20px;"></td></tr>
|
||||
|
||||
<!-- TL;DR Section -->
|
||||
<tr>
|
||||
<td style="padding: 30px 40px;">
|
||||
<h2 style="margin: 0 0 8px 0; font-size: 22px; font-weight: 700; color: #1a1a1a;">
|
||||
📋 TL;DR - Quick Summary
|
||||
</h2>
|
||||
<p style="margin: 0 0 20px 0; font-size: 13px; color: #999999;">
|
||||
Scan through today's top stories in seconds
|
||||
</p>
|
||||
|
||||
<!-- TL;DR Box -->
|
||||
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0" style="background-color: #f8f9fa; border-radius: 8px; border-left: 4px solid #667eea;">
|
||||
<td style="background-color: #ffffff; border-radius: 16px; padding: 30px; box-shadow: 0 2px 4px rgba(0,0,0,0.05);">
|
||||
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0">
|
||||
<tr>
|
||||
<td style="padding: 20px 25px;">
|
||||
<td style="padding-bottom: 20px; border-bottom: 2px solid #f0f2f5;">
|
||||
<h2 style="margin: 0; font-size: 20px; font-weight: 700; color: #1a1a1a;">
|
||||
⚡️ Quick Summary
|
||||
</h2>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding-top: 20px;">
|
||||
{% for section in category_sections %}
|
||||
{% for article in section.articles %}
|
||||
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0" style="margin-bottom: 15px;">
|
||||
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0" style="margin-bottom: 12px;">
|
||||
<tr>
|
||||
<td style="width: 30px; vertical-align: top; padding-top: 2px;">
|
||||
<span style="display: inline-block; background-color: #667eea; color: #ffffff; width: 22px; height: 22px; line-height: 22px; text-align: center; border-radius: 50%; font-size: 11px; font-weight: 700;">
|
||||
{{ loop.index }}
|
||||
</span>
|
||||
<td style="width: 24px; vertical-align: top; padding-top: 4px;">
|
||||
<div style="height: 6px; width: 6px; background-color: #667eea; border-radius: 50%;"></div>
|
||||
</td>
|
||||
<td style="vertical-align: top;">
|
||||
<p style="margin: 0; font-size: 14px; line-height: 1.6; color: #333333;">
|
||||
<strong style="color: #1a1a1a;">{{ article.title_en if article.title_en else article.title }}</strong>
|
||||
<br>
|
||||
<span style="color: #666666;">{{ article.summary.split('.')[0] }}.</span>
|
||||
<p style="margin: 0; font-size: 15px; line-height: 1.5; color: #4a4a4a;">
|
||||
<strong style="color: #1a1a1a;">{{ article.title_en if article.title_en else article.title }}</strong> — {{ article.summary.split('.')[0] }}.
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
{% if not loop.last %}
|
||||
<div style="height: 1px; background-color: #e0e0e0; margin: 10px 0;"></div>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
</td>
|
||||
@@ -127,88 +121,48 @@
|
||||
</tr>
|
||||
|
||||
{% if transport_disruptions and transport_disruptions|length > 0 %}
|
||||
<!-- Divider -->
|
||||
<tr><td style="height: 20px;"></td></tr>
|
||||
<tr>
|
||||
<td style="padding: 0 40px;">
|
||||
<div style="height: 2px; background-color: #e0e0e0;"></div>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- Transport Disruptions Section -->
|
||||
<tr>
|
||||
<td style="padding: 30px 40px;">
|
||||
<h2 style="margin: 0 0 20px 0; font-size: 22px; font-weight: 700; color: #1a1a1a;">
|
||||
🚆 S-Bahn Disruptions Today
|
||||
<td style="background-color: #ffffff; border-radius: 16px; padding: 30px; box-shadow: 0 2px 4px rgba(0,0,0,0.05);">
|
||||
<h2 style="margin: 0 0 15px 0; font-size: 20px; font-weight: 700; color: #1a1a1a;">
|
||||
🚆 S-Bahn Updates
|
||||
</h2>
|
||||
<p style="margin: 0 0 20px 0; font-size: 14px; color: #666666;">
|
||||
Current service disruptions affecting Munich S-Bahn:
|
||||
</p>
|
||||
|
||||
{% for disruption in transport_disruptions %}
|
||||
<!-- Disruption Card -->
|
||||
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0" style="margin-bottom: 15px; background-color: #fff8f0; border-left: 4px solid #ff9800; border-radius: 4px;">
|
||||
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0" style="background-color: #fff8f0; border-radius: 8px; border: 1px solid #ffeeba; margin-bottom: 10px;">
|
||||
<tr>
|
||||
<td style="padding: 15px 20px;">
|
||||
<!-- Severity and Lines -->
|
||||
<p style="margin: 0 0 8px 0; font-size: 13px; color: #666666;">
|
||||
{{ disruption.severity_icon }} <strong style="color: #000000;">{{ disruption.lines_str }}</strong>
|
||||
<td style="padding: 15px;">
|
||||
<p style="margin: 0 0 5px 0; font-size: 12px; font-weight: 700; color: #c05621; text-transform: uppercase;">
|
||||
{{ disruption.severity_icon }} {{ disruption.lines_str }}
|
||||
</p>
|
||||
|
||||
<!-- Title -->
|
||||
<p style="margin: 0 0 8px 0; font-size: 15px; font-weight: 700; color: #1a1a1a; line-height: 1.4;">
|
||||
<p style="margin: 0 0 5px 0; font-size: 15px; font-weight: 700; color: #2d3748;">
|
||||
{{ disruption.title }}
|
||||
</p>
|
||||
|
||||
<!-- Description -->
|
||||
{% if disruption.description %}
|
||||
<p style="margin: 0 0 8px 0; font-size: 14px; color: #333333; line-height: 1.5;">
|
||||
<p style="margin: 0; font-size: 14px; color: #4a5568; line-height: 1.4;">
|
||||
{{ disruption.description }}
|
||||
</p>
|
||||
{% endif %}
|
||||
|
||||
<!-- Time -->
|
||||
{% if disruption.start_time_str or disruption.end_time_str %}
|
||||
<p style="margin: 0; font-size: 13px; color: #666666;">
|
||||
⏰
|
||||
{% if disruption.start_time_str %}
|
||||
From {{ disruption.start_time_str }}
|
||||
{% endif %}
|
||||
{% if disruption.end_time_str %}
|
||||
until {{ disruption.end_time_str }}
|
||||
{% endif %}
|
||||
</p>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
{% endfor %}
|
||||
|
||||
<p style="margin: 15px 0 0 0; font-size: 12px; color: #999999; font-style: italic;">
|
||||
💡 Plan your commute accordingly. Check <a href="https://www.mvg.de" style="color: #667eea; text-decoration: none;">MVG.de</a> for real-time updates.
|
||||
<p style="margin: 10px 0 0 0; font-size: 13px; color: #718096;">
|
||||
Check <a href="https://www.mvg.de" style="color: #667eea; text-decoration: underline;">MVG.de</a> for live times.
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
|
||||
<!-- Divider -->
|
||||
<tr>
|
||||
<td style="padding: 0 40px;">
|
||||
<div style="height: 2px; background-color: #e0e0e0;"></div>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- Category Sections -->
|
||||
{% for section in category_sections %}
|
||||
|
||||
<tr>
|
||||
<td style="padding: 30px 40px 15px 40px;">
|
||||
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0">
|
||||
<td align="center" style="padding: 40px 0 20px 0;">
|
||||
<table role="presentation" border="0" cellspacing="0" cellpadding="0">
|
||||
<tr>
|
||||
<td>
|
||||
<h2 style="margin: 0; font-size: 22px; font-weight: 700; color: #1a1a1a;">
|
||||
{{ section.icon }} {{ section.name }}
|
||||
</h2>
|
||||
<p style="margin: 8px 0 0 0; font-size: 13px; color: #666666;">
|
||||
Top stories in {{ section.name.lower() }}
|
||||
<td align="center" style="background-color: #e2e8f0; color: #4a5568; border-radius: 20px; padding: 8px 20px;">
|
||||
<p style="margin: 0; font-size: 13px; font-weight: 700; letter-spacing: 0.5px; text-transform: uppercase;">
|
||||
{{ section.icon }} {{ section.name }}
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -216,155 +170,99 @@
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- Category Articles -->
|
||||
{% for article in section.articles %}
|
||||
<tr>
|
||||
<td style="padding: 25px 40px;">
|
||||
<!-- Article Number Badge -->
|
||||
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0">
|
||||
<tr>
|
||||
<td>
|
||||
<span style="display: inline-block; background-color: #000000; color: #ffffff; width: 24px; height: 24px; line-height: 24px; text-align: center; border-radius: 50%; font-size: 12px; font-weight: 600;">
|
||||
{{ loop.index }}
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<td style="background-color: #ffffff; border-radius: 16px; padding: 30px; margin-bottom: 20px; box-shadow: 0 2px 4px rgba(0,0,0,0.05);">
|
||||
<p style="margin: 0 0 12px 0; font-size: 12px; color: #718096; font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px;">
|
||||
{% if article.is_clustered %}
|
||||
Multiple Sources
|
||||
{% else %}
|
||||
{{ article.source }}
|
||||
{% endif %}
|
||||
</p>
|
||||
|
||||
<!-- Article Title -->
|
||||
<h2 style="margin: 12px 0 8px 0; font-size: 19px; font-weight: 700; line-height: 1.3; color: #1a1a1a;">
|
||||
{{ article.title_en if article.title_en else article.title }}
|
||||
</h2>
|
||||
<h3 style="margin: 0 0 10px 0; font-size: 22px; font-weight: 700; line-height: 1.3; color: #1a1a1a;">
|
||||
<a href="{{ article.link }}" style="color: #1a1a1a; text-decoration: none;">
|
||||
{{ article.title_en if article.title_en else article.title }}
|
||||
</a>
|
||||
</h3>
|
||||
|
||||
<!-- Original German Title (subtitle) -->
|
||||
{% if article.title_en and article.title_en != article.title %}
|
||||
<p style="margin: 0 0 12px 0; font-size: 13px; color: #999999; font-style: italic;">
|
||||
Original: {{ article.title }}
|
||||
<p style="margin: 0 0 15px 0; font-size: 13px; color: #a0aec0; font-style: italic;">
|
||||
"{{ article.title }}"
|
||||
</p>
|
||||
{% endif %}
|
||||
|
||||
<!-- Article Meta -->
|
||||
<p style="margin: 0 0 12px 0; font-size: 13px; color: #999999;">
|
||||
{% if article.is_clustered %}
|
||||
<span style="color: #000000; font-weight: 600;">Multiple sources</span>
|
||||
{% else %}
|
||||
<span style="color: #000000; font-weight: 600;">{{ article.source }}</span>
|
||||
{% if article.author %}
|
||||
<span> • {{ article.author }}</span>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</p>
|
||||
|
||||
<!-- Article Summary -->
|
||||
<p style="margin: 0 0 15px 0; font-size: 15px; line-height: 1.6; color: #333333; text-align: justify;">
|
||||
<p style="margin: 0 0 20px 0; font-size: 16px; line-height: 1.6; color: #4a5568;">
|
||||
{{ article.summary }}
|
||||
</p>
|
||||
|
||||
<!-- Read More Links -->
|
||||
{% if article.is_clustered and article.sources %}
|
||||
<!-- Multiple sources -->
|
||||
<p style="margin: 0 0 8px 0; font-size: 13px; color: #666666;">
|
||||
📰 Covered by {{ article.article_count }} sources:
|
||||
</p>
|
||||
<div style="margin: 0;">
|
||||
{% for source in article.sources %}
|
||||
<a href="{{ source.link }}" style="display: inline-block; color: #000000; text-decoration: none; font-size: 13px; font-weight: 600; border-bottom: 2px solid #000000; padding-bottom: 2px; margin-right: 15px; margin-bottom: 8px;">
|
||||
{{ source.name }} →
|
||||
</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<!-- Single source -->
|
||||
<a href="{{ article.link }}" style="display: inline-block; color: #000000; text-decoration: none; font-size: 14px; font-weight: 600; border-bottom: 2px solid #000000; padding-bottom: 2px;">
|
||||
Read more →
|
||||
</a>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- Article Divider -->
|
||||
{% if not loop.last %}
|
||||
<tr>
|
||||
<td style="padding: 0 40px;">
|
||||
<div style="height: 1px; background-color: #f0f0f0;"></div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
<!-- Category Section Divider -->
|
||||
{% if not loop.last %}
|
||||
<tr>
|
||||
<td style="padding: 25px 40px;">
|
||||
<div style="height: 2px; background-color: #e0e0e0;"></div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
<!-- Bottom Divider -->
|
||||
<tr>
|
||||
<td style="padding: 25px 40px 0 40px;">
|
||||
<div style="height: 1px; background-color: #e0e0e0;"></div>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<td style="padding: 30px 40px;">
|
||||
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0" style="background-color: #f8f8f8; border-radius: 8px;">
|
||||
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0">
|
||||
<tr>
|
||||
<td style="padding: 25px; text-align: center;">
|
||||
<p style="margin: 0 0 8px 0; font-size: 13px; color: #666666; text-transform: uppercase; letter-spacing: 1px; font-weight: 600;">
|
||||
Today's Digest
|
||||
</p>
|
||||
<p style="margin: 0; font-size: 36px; font-weight: 700; color: #000000;">
|
||||
{{ article_count }}
|
||||
</p>
|
||||
<p style="margin: 8px 0 0 0; font-size: 14px; color: #666666;">
|
||||
stories • AI-summarized • 5 min read
|
||||
</p>
|
||||
<td>
|
||||
{% if article.is_clustered and article.sources %}
|
||||
<p style="margin: 0; font-size: 13px; color: #718096;">
|
||||
<strong>Read full coverage:</strong><br>
|
||||
{% for source in article.sources %}
|
||||
<a href="{{ source.link }}" style="color: #667eea; text-decoration: none; margin-right: 10px; display: inline-block; margin-top: 5px;">
|
||||
{{ source.name }} ↗
|
||||
</a>
|
||||
{% endfor %}
|
||||
</p>
|
||||
{% else %}
|
||||
<table role="presentation" cellpadding="0" cellspacing="0" border="0">
|
||||
<tr>
|
||||
<td style="background-color: #f0f2f5; border-radius: 8px; padding: 10px 20px;">
|
||||
<a href="{{ article.link }}" style="color: #1a1a1a; font-size: 14px; font-weight: 600; text-decoration: none; display: block;">
|
||||
Read full story →
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
<!-- Footer -->
|
||||
</tr>
|
||||
<tr><td style="height: 20px;"></td></tr>
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
|
||||
<tr>
|
||||
<td style="background-color: #1a1a1a; padding: 30px 40px; text-align: center;">
|
||||
<p style="margin: 0 0 15px 0; font-size: 14px; color: #ffffff; font-weight: 600;">
|
||||
Munich News Daily
|
||||
<td align="center" style="padding: 40px 0;">
|
||||
<p style="margin: 0 0 5px 0; font-size: 48px; font-weight: 800; color: #cbd5e0; line-height: 1;">
|
||||
{{ article_count }}
|
||||
</p>
|
||||
<p style="margin: 0 0 20px 0; font-size: 13px; color: #999999; line-height: 1.5;">
|
||||
AI-powered news summaries for busy people.<br>
|
||||
Delivered daily to your inbox.
|
||||
<p style="margin: 0; font-size: 14px; font-weight: 600; color: #a0aec0; text-transform: uppercase; letter-spacing: 1px;">
|
||||
Stories Summarized
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td style="text-align: center; padding: 0 20px 40px 20px;">
|
||||
<p style="margin: 0 0 20px 0; font-size: 13px; line-height: 1.5; color: #718096;">
|
||||
<strong>Munich News Daily</strong><br>
|
||||
AI-powered news for busy locals.
|
||||
</p>
|
||||
|
||||
<!-- Footer Links -->
|
||||
<p style="margin: 0; font-size: 12px; color: #666666;">
|
||||
<a href="{{ website_link }}" style="color: #999999; text-decoration: none;">Visit Website</a>
|
||||
<span style="color: #444444;"> • </span>
|
||||
<a href="{{ preferences_link }}" style="color: #999999; text-decoration: none;">Manage Preferences</a>
|
||||
<span style="color: #444444;"> • </span>
|
||||
<a href="{{ unsubscribe_link }}" style="color: #999999; text-decoration: none;">Unsubscribe</a>
|
||||
<p style="margin: 0 0 20px 0; font-size: 12px; color: #a0aec0;">
|
||||
<a href="{{ website_link }}" style="color: #718096; text-decoration: underline;">Website</a> •
|
||||
<a href="{{ preferences_link }}" style="color: #718096; text-decoration: underline;">Preferences</a> •
|
||||
<a href="{{ unsubscribe_link }}" style="color: #718096; text-decoration: underline;">Unsubscribe</a>
|
||||
</p>
|
||||
|
||||
{% if tracking_enabled %}
|
||||
<!-- Privacy Notice -->
|
||||
<p style="margin: 20px 0 0 0; font-size: 11px; color: #666666; line-height: 1.4;">
|
||||
This email contains tracking to measure engagement and improve our content.<br>
|
||||
We respect your privacy and anonymize data after 90 days.
|
||||
</p>
|
||||
{% endif %}
|
||||
|
||||
<p style="margin: 20px 0 0 0; font-size: 11px; color: #666666;">
|
||||
© {{ year }} Munich News Daily. All rights reserved.
|
||||
|
||||
<p style="margin: 0; font-size: 11px; color: #cbd5e0;">
|
||||
© {{ year }} Munich News Daily. Made with 🥨 in Bavaria.
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
</table>
|
||||
<!-- End Main Container -->
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<!-- End Wrapper Table -->
|
||||
|
||||
</body>
|
||||
</html>
|
||||
</html>
|
||||
Reference in New Issue
Block a user