feat: news ticker shows flagged hourly items

This commit is contained in:
Sirius DevOps 2026-08-27 23:36:40 -04:00
parent 610666ef6a
commit 3b4dd49253

View file

@ -539,11 +539,14 @@
.tick-item .dom { font-family: 'Share Tech Mono', monospace; font-size: 0.64rem; color: var(--cyan); background: rgba(53,224,255,0.08); border: 1px solid rgba(53,224,255,0.22); border-radius: 3px; padding: 0.05rem 0.4rem; }
.tick-item .tt { font-family: 'Share Tech Mono', monospace; font-size: 0.66rem; color: var(--muted); }
.tick-item .sep { color: var(--line-hi); }
button.tick-item { cursor: pointer; background: transparent; border: 0; font: inherit; color: inherit; }
.tick-item.brief {
cursor: pointer; background: linear-gradient(90deg, rgba(255,46,151,0.12), transparent 70%);
border-left: 2px solid var(--magenta); font-weight: 600;
}
.tick-item.brief .b-tag { font-family: 'Share Tech Mono', monospace; font-size: 0.6rem; color: #1c0311; background: var(--magenta); border-radius: 2px; padding: 0.08rem 0.4rem; letter-spacing: 0.1em; }
.tick-item .b-tag { font-family: 'Share Tech Mono', monospace; font-size: 0.6rem; color: #1c0311; background: var(--magenta); border-radius: 2px; padding: 0.08rem 0.4rem; letter-spacing: 0.1em; }
.tick-item .b-tag.critical { background: var(--red); }
.tick-item .b-tag.high { background: var(--amber); }
.tick-item.brief:hover { color: var(--magenta); }
.tick-item.standby { color: var(--muted); opacity: 0.75; }
.tick-item.standby .price { color: var(--muted); }
@ -827,7 +830,7 @@
<div class="ns-kicker">Executive Summary <span class="ns-badge" id="ns-badge">WAITING</span></div>
<div class="ns-time" id="ns-time"></div>
<div class="ns-body" id="ns-body">
<div class="ns-empty">No executive summary yet — the Gemini summarizer is idle until <code>GEMINI_API_KEY</code> is set on the Pi.</div>
<div class="ns-empty">No executive summary yet — the Nous summarizer is idle until <code>NOUS_API_KEY</code> is set on the Pi and a model is chosen in Settings.</div>
</div>
</div>
@ -1323,13 +1326,16 @@ async function loadNews(force) {
if (upd) upd.textContent = 'updating…';
let articles = [];
let summaries = [];
let tickerItems = [];
try {
const [rArticles, rSumm] = await Promise.all([
const [rArticles, rSumm, rTick] = await Promise.all([
fetch(`${API}/api/news?limit=100`),
fetch(`${API}/api/news/summaries?limit=1`)
fetch(`${API}/api/news/summaries?limit=1`),
fetch(`${API}/api/news/ticker?limit=20`),
]);
if (rArticles.ok) articles = await rArticles.json();
if (rSumm.ok) summaries = await rSumm.json();
if (rTick.ok) tickerItems = await rTick.json();
} catch (e) {
const list = document.getElementById('news-list');
const body = document.getElementById('ns-body');
@ -1341,7 +1347,7 @@ async function loadNews(force) {
}
renderNewsSummary(summaries);
renderNewsList(articles);
renderNewsTicker(articles, summaries);
renderNewsTicker(articles, summaries, tickerItems);
if (upd) upd.textContent = 'updated ' + new Date().toLocaleTimeString();
if (!newsInterval) {
newsInterval = setInterval(() => loadNews(false), NEWS_REFRESH_MS);
@ -1353,7 +1359,7 @@ function renderNewsSummary(summaries) {
const badgeEl = document.getElementById('ns-badge');
if (!bodyEl) return;
if (!summaries || !summaries.length) {
bodyEl.innerHTML = '<div class="ns-empty">No executive summary yet — the Gemini summarizer is idle until <code>GEMINI_API_KEY</code> is set on the Pi.</div>';
bodyEl.innerHTML = '<div class="ns-empty">No executive summary yet — the Nous summarizer is idle until <code>NOUS_API_KEY</code> is set on the Pi and a model is chosen in Settings.</div>';
timeEl.textContent = '—';
badgeEl.textContent = 'WAITING';
return;
@ -1381,25 +1387,45 @@ function renderNewsList(articles) {
'</a>';
}).join('');
}
function renderNewsTicker(articles, summaries) {
function renderNewsTicker(articles, summaries, tickerItems) {
const track = document.getElementById('nt-track');
if (!track) return;
let items = '';
// LLM executive summary flash first (click → News view)
const summ = summaries && summaries.length ? summaries[0] : null;
if (summ && summ.summary_text) {
const brief = summ.summary_text.replace(/\s+/g, ' ').trim();
items += `<button class="tick-item brief" onclick="showView('news')" title="Open full brief">` +
`<span class="b-tag">LLM BRIEF</span><span>${newsEsc(brief.slice(0, 200))}${brief.length > 200 ? '…' : ''}</span>` +
`<span class="tt">${newsTimeAgo(summ.batch_timestamp)}</span><span class="sep"></span></button>`;
const ticks = Array.isArray(tickerItems) ? tickerItems : [];
if (ticks.length) {
ticks.forEach(t => {
const imp = String(t.importance || '').toLowerCase();
const tag = imp === 'critical' ? 'CRITICAL' : 'HIGH';
const loc = (t.location_name || '').trim();
const inner =
`<span class="b-tag ${imp === 'critical' ? 'critical' : 'high'}">${tag}</span>` +
`<span>${newsEsc((t.headline || '').trim())}</span>` +
(loc ? `<span class="dom">${newsEsc(loc)}</span>` : '') +
`<span class="tt">${newsTimeAgo(t.created_at)}</span><span class="sep"></span>`;
const url = (t.url || '').trim();
if (url) {
items += `<a class="tick-item" href="${newsEsc(url)}" target="_blank" rel="noopener">${inner}</a>`;
} else {
items += `<button class="tick-item" onclick="showView('news')" title="Open news brief">${inner}</button>`;
}
});
} else {
// Quiet hour: brief chip + article titles so the dock is never blank.
const summ = summaries && summaries.length ? summaries[0] : null;
const briefSrc = summ && (summ.summary_en || summ.summary_text);
if (briefSrc) {
const brief = String(briefSrc).replace(/\s+/g, ' ').trim();
items += `<button class="tick-item brief" onclick="showView('news')" title="Open full brief">` +
`<span class="b-tag">LLM BRIEF</span><span>${newsEsc(brief.slice(0, 120))}${brief.length > 120 ? '…' : ''}</span>` +
`<span class="tt">${newsTimeAgo(summ.batch_timestamp)}</span><span class="sep"></span></button>`;
}
(articles || []).slice(0, 10).forEach(a => {
items += `<a class="tick-item" href="${newsEsc(a.url || '#')}" target="_blank" rel="noopener">` +
`<span class="dom">${newsEsc(a.domain || '?')}</span>` +
`<span>${newsEsc((a.title || '').trim())}</span>` +
`<span class="tt">${newsTimeAgo(a.timestamp)}</span><span class="sep"></span></a>`;
});
}
// Top headlines
(articles || []).slice(0, 30).forEach(a => {
items += `<a class="tick-item" href="${newsEsc(a.url || '#')}" target="_blank" rel="noopener">` +
`<span class="dom">${newsEsc(a.domain || '?')}</span>` +
`<span>${newsEsc((a.title || '').trim())}</span>` +
`<span class="tt">${newsTimeAgo(a.timestamp)}</span><span class="sep"></span></a>`;
});
if (!items) {
items = `<span class="tick-item standby">No headlines yet — scraper idle</span>`;
}