Files
aitrade/pkg/app/web/templates/news.html
T
kaedwen ea141eb012
Build and Push Docker Image / build-and-push (push) Failing after 1m41s
initial
2026-07-02 20:09:44 +02:00

345 lines
9.0 KiB
HTML

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>{{.Title}}</title>
<link rel="stylesheet" href="/static/style.css">
</head>
<body>
<div class="container">
<div class="news-container">
<div class="news-header">
<h1>📰 Live News Feed</h1>
<div class="news-stats">
<span id="news-count">0 articles</span>
<span id="last-update">Last update: never</span>
</div>
</div>
<div class="news-filters">
<button class="filter-btn active" data-filter="all">All</button>
<button class="filter-btn" data-filter="positive">Positive</button>
<button class="filter-btn" data-filter="neutral">Neutral</button>
<button class="filter-btn" data-filter="negative">Negative</button>
</div>
<div id="news-list" class="news-list">
<p class="loading">Loading news...</p>
</div>
</div>
</div>
<script src="/static/app.js"></script>
<style>
.news-container {
max-width: 1200px;
margin: 0 auto;
}
.news-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
padding-bottom: 10px;
border-bottom: 2px solid #333;
}
.news-stats {
display: flex;
gap: 20px;
font-size: 0.9em;
color: #666;
}
.news-filters {
display: flex;
gap: 10px;
margin-bottom: 20px;
}
.filter-btn {
padding: 8px 16px;
border: 2px solid #ddd;
background: white;
cursor: pointer;
border-radius: 4px;
transition: all 0.2s;
}
.filter-btn:hover {
background: #f0f0f0;
}
.filter-btn.active {
background: #007bff;
color: white;
border-color: #007bff;
}
.news-list {
display: flex;
flex-direction: column;
gap: 15px;
}
.news-article {
padding: 15px;
border-radius: 8px;
border-left: 4px solid #ccc;
background: white;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
transition: transform 0.2s;
}
.news-article:hover {
transform: translateX(5px);
}
.news-article.positive {
border-left-color: #28a745;
background: #f0fff4;
}
.news-article.negative {
border-left-color: #dc3545;
background: #fff5f5;
}
.news-article.neutral {
border-left-color: #6c757d;
background: #f8f9fa;
}
.news-article-header {
display: flex;
justify-content: space-between;
align-items: start;
margin-bottom: 10px;
}
.news-title {
font-size: 1.1em;
font-weight: bold;
margin: 0;
flex: 1;
}
.news-title a {
color: #333;
text-decoration: none;
}
.news-title a:hover {
color: #007bff;
text-decoration: underline;
}
.news-sentiment {
display: flex;
align-items: center;
gap: 8px;
margin-left: 15px;
}
.sentiment-badge {
padding: 4px 12px;
border-radius: 12px;
font-size: 0.85em;
font-weight: bold;
white-space: nowrap;
}
.sentiment-badge.positive {
background: #28a745;
color: white;
}
.sentiment-badge.negative {
background: #dc3545;
color: white;
}
.sentiment-badge.neutral {
background: #6c757d;
color: white;
}
.sentiment-score {
font-size: 0.9em;
color: #666;
font-weight: bold;
}
.news-meta {
display: flex;
gap: 15px;
font-size: 0.85em;
color: #666;
margin-bottom: 8px;
}
.news-source {
font-weight: bold;
color: #007bff;
}
.news-time {
color: #999;
}
.news-content {
color: #555;
line-height: 1.5;
margin-top: 8px;
}
.news-symbols {
margin-top: 10px;
display: flex;
gap: 8px;
flex-wrap: wrap;
}
.symbol-tag {
padding: 3px 8px;
background: #e9ecef;
border-radius: 4px;
font-size: 0.85em;
font-weight: bold;
color: #495057;
}
.news-method {
font-size: 0.8em;
color: #999;
font-style: italic;
margin-top: 5px;
}
.loading {
text-align: center;
padding: 40px;
color: #999;
}
.no-news {
text-align: center;
padding: 40px;
color: #999;
font-style: italic;
}
</style>
<script>
let currentFilter = 'all';
let allNews = [];
function formatTimeAgo(dateString) {
const date = new Date(dateString);
const now = new Date();
const seconds = Math.floor((now - date) / 1000);
if (seconds < 60) return 'just now';
if (seconds < 3600) return Math.floor(seconds / 60) + 'm ago';
if (seconds < 86400) return Math.floor(seconds / 3600) + 'h ago';
return Math.floor(seconds / 86400) + 'd ago';
}
function formatScore(score) {
if (score === null || score === undefined) return '';
return (score > 0 ? '+' : '') + score.toFixed(2);
}
function getSentimentLabel(article) {
return article.SentimentLabel || 'neutral';
}
function renderNews(articles) {
const container = document.getElementById('news-list');
if (!articles || articles.length === 0) {
container.innerHTML = '<p class="no-news">No news articles available</p>';
return;
}
const filtered = currentFilter === 'all'
? articles
: articles.filter(a => getSentimentLabel(a) === currentFilter);
if (filtered.length === 0) {
container.innerHTML = '<p class="no-news">No ' + currentFilter + ' articles</p>';
return;
}
container.innerHTML = filtered.map(article => {
const sentiment = getSentimentLabel(article);
const score = article.SentimentScore;
const symbols = article.Symbols ? article.Symbols.split(',').filter(s => s.trim()) : [];
const method = article.SentimentMethod || 'keyword';
return `
<div class="news-article ${sentiment}">
<div class="news-article-header">
<h3 class="news-title">
<a href="${article.URL}" target="_blank" rel="noopener">${article.Title}</a>
</h3>
<div class="news-sentiment">
${score !== null && score !== undefined ?
`<span class="sentiment-score">${formatScore(score)}</span>` : ''}
<span class="sentiment-badge ${sentiment}">${sentiment}</span>
</div>
</div>
<div class="news-meta">
<span class="news-source">${article.Source}</span>
<span class="news-time">${formatTimeAgo(article.PublishedAt)}</span>
</div>
${article.Content ? `<div class="news-content">${article.Content}</div>` : ''}
${symbols.length > 0 ? `
<div class="news-symbols">
${symbols.map(s => `<span class="symbol-tag">${s.trim()}</span>`).join('')}
</div>
` : ''}
<div class="news-method">Analysis: ${method}</div>
</div>
`;
}).join('');
// Update stats
document.getElementById('news-count').textContent = filtered.length + ' article' + (filtered.length !== 1 ? 's' : '');
document.getElementById('last-update').textContent = 'Last update: ' + new Date().toLocaleTimeString();
}
function loadNews() {
fetch('/api/news/recent?limit=50')
.then(response => response.json())
.then(data => {
allNews = data || [];
renderNews(allNews);
})
.catch(error => {
console.error('Failed to load news:', error);
document.getElementById('news-list').innerHTML =
'<p class="no-news">Failed to load news. Please try again later.</p>';
});
}
// Filter buttons
document.querySelectorAll('.filter-btn').forEach(btn => {
btn.addEventListener('click', function() {
document.querySelectorAll('.filter-btn').forEach(b => b.classList.remove('active'));
this.classList.add('active');
currentFilter = this.dataset.filter;
renderNews(allNews);
});
});
// Initial load
loadNews();
// Auto-refresh every 30 seconds
setInterval(loadNews, 30000);
</script>
</body>
</html>