/* ─── SITREP: Daily AI Briefing Frontend ─────────── */ (function() { 'use strict'; const API_BASE = '/api/sitrep'; let archiveDates = []; let currentDate = null; // ─── DOM Elements ──────────────────────────────── const els = { dateTitle: document.getElementById('sitrepDateTitle'), notice: document.getElementById('sitrepNotice'), noticeText: document.getElementById('sitrepNoticeText'), cryptoBar: document.getElementById('sitrepCryptoBar'), briefing: document.getElementById('sitrepBriefing'), content: document.getElementById('sitrepContent'), metaSources: document.getElementById('metaSources'), metaModel: document.getElementById('metaModel'), metaGenerated: document.getElementById('metaGenerated'), archiveList: document.getElementById('sitrepArchiveList'), btnPrev: document.getElementById('sitrepPrev'), btnNext: document.getElementById('sitrepNext'), btnGenerate: document.getElementById('sitrepGenerate') }; // ─── Markdown to HTML ──────────────────────────── function mdToHtml(md) { if (!md) return '
No content available.
'; let html = md; // Escape HTML entities (but preserve markdown) html = html.replace(/&/g, '&').replace(//g, '>'); // Restore markdown-safe chars html = html.replace(/> /gm, '> '); // blockquotes // Horizontal rules html = html.replace(/^---+$/gm, '$1');
// Links [text](url)
html = html.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '$1');
// Blockquotes (multi-line support)
html = html.replace(/^> (.+)$/gm, '$1'); // Merge consecutive blockquotes html = html.replace(/<\/blockquote>\n
/g, '
'); // Unordered lists html = html.replace(/^[\-\*] (.+)$/gm, '$1 '); // Wrap consecutivein html = html.replace(/((?:
- .*<\/li>\n?)+)/g, '
$1
'); // Ordered lists html = html.replace(/^\d+\. (.+)$/gm, '$1 '); html = html.replace(/((?:.*<\/oli>\n?)+)/g, function(match) { return ' ' + match.replace(/<\/?oli>/g, function(tag) { return tag.replace('oli', 'li'); }) + '
'; }); // Paragraphs — wrap remaining loose text lines const lines = html.split('\n'); const result = []; let inBlock = false; for (let i = 0; i < lines.length; i++) { const line = lines[i].trim(); if (!line) { if (!inBlock) result.push(''); continue; } // Skip if already an HTML block element if (/^<(h[1-6]|ul|ol|li|blockquote|hr|p|div)/.test(line)) { result.push(line); inBlock = /^<(ul|ol|blockquote)/.test(line); } else if (/<\/(ul|ol|blockquote)>$/.test(line)) { result.push(line); inBlock = false; } else { result.push('' + line + '
'); } } return result.join('\n'); } // ─── Date Formatting ───────────────────────────── function formatSitrepDate(dateStr) { if (!dateStr) return 'UNKNOWN DATE'; const d = new Date(dateStr + 'T00:00:00Z'); const months = ['JAN','FEB','MAR','APR','MAY','JUN','JUL','AUG','SEP','OCT','NOV','DEC']; const day = String(d.getUTCDate()).padStart(2, '0'); const month = months[d.getUTCMonth()]; const year = d.getUTCFullYear(); return `DAILY SITREP // ${day} ${month} ${year} // 0700 HRS`; } function formatArchiveDate(dateStr) { if (!dateStr) return '???'; const d = new Date(dateStr + 'T00:00:00Z'); const months = ['JAN','FEB','MAR','APR','MAY','JUN','JUL','AUG','SEP','OCT','NOV','DEC']; const day = String(d.getUTCDate()).padStart(2, '0'); return `${day} ${months[d.getUTCMonth()]} ${d.getUTCFullYear()}`; } function formatTimestamp(ts) { if (!ts) return '—'; const d = new Date(ts); return d.toUTCString().replace('GMT', 'UTC'); } // ─── Crypto Ticker ─────────────────────────────── function renderCrypto(crypto) { if (!crypto || !els.cryptoBar) return; const tickers = Object.entries(crypto); if (!tickers.length) { els.cryptoBar.style.display = 'none'; return; } let html = ''; tickers.forEach(([symbol, data], idx) => { const price = parseFloat(data.price) || 0; const change = parseFloat(data.change) || 0; const direction = change >= 0 ? '▲' : '▼'; const cls = change >= 0 ? 'positive' : 'negative'; html += `${symbol} $${price.toLocaleString('en-US', {minimumFractionDigits: 2, maximumFractionDigits: 2})} ${direction} ${change >= 0 ? '+' : ''}${change.toFixed(2)}%`; if (idx < tickers.length - 1) { html += ''; } }); els.cryptoBar.innerHTML = html; els.cryptoBar.style.display = 'flex'; } // ─── Navigation ────────────────────────────────── function updateNav() { if (!archiveDates.length || !currentDate) { if (els.btnPrev) els.btnPrev.classList.add('disabled'); if (els.btnNext) els.btnNext.classList.add('disabled'); return; } const dates = archiveDates.map(d => d.date); const idx = dates.indexOf(currentDate); if (els.btnPrev) { if (idx < dates.length - 1) { els.btnPrev.classList.remove('disabled'); els.btnPrev.onclick = () => loadSitrep(dates[idx + 1]); } else { els.btnPrev.classList.add('disabled'); els.btnPrev.onclick = null; } } if (els.btnNext) { if (idx > 0) { els.btnNext.classList.remove('disabled'); els.btnNext.onclick = () => loadSitrep(dates[idx - 1]); } else { els.btnNext.classList.add('disabled'); els.btnNext.onclick = null; } } } function updateArchiveHighlight() { if (!els.archiveList) return; els.archiveList.querySelectorAll('.sitrep-archive-item').forEach(item => { item.classList.toggle('active', item.dataset.date === currentDate); }); } // ─── Load SITREP ───────────────────────────────── async function loadSitrep(date) { if (els.content) { els.content.innerHTML = 'DECRYPTING TRANSMISSION...'; } try { const url = date ? `${API_BASE}?date=${date}` : API_BASE; const resp = await fetch(url); if (!resp.ok) { const err = await resp.json().catch(() => ({})); if (els.content) { els.content.innerHTML = `⚠ ${err.error || 'SITREP not available'}`; } if (els.cryptoBar) els.cryptoBar.style.display = 'none'; return; } const data = await resp.json(); currentDate = data.date; // Date header if (els.dateTitle) { els.dateTitle.textContent = formatSitrepDate(data.date); } // Notice if (els.notice && els.noticeText) { if (data.notice) { els.noticeText.textContent = data.notice; els.notice.classList.add('visible'); } else { els.notice.classList.remove('visible'); } } // Crypto bar renderCrypto(data.crypto); // Briefing content if (els.content) { els.content.innerHTML = mdToHtml(data.content); } // Meta if (els.metaSources) els.metaSources.textContent = data.sources_used || '—'; if (els.metaModel) els.metaModel.textContent = data.model || '—'; if (els.metaGenerated) els.metaGenerated.textContent = formatTimestamp(data.generated_at); // Nav updateNav(); updateArchiveHighlight(); } catch (e) { console.error('SITREP load error:', e); if (els.content) { els.content.innerHTML = '⚠ TRANSMISSION FAILED — Unable to reach API'; } } } // ─── Load Archive List ──────────────────────────── async function loadArchive() { if (!els.archiveList) return; try { const resp = await fetch(`${API_BASE}/list`); if (!resp.ok) return; const data = await resp.json(); archiveDates = data.dates || []; if (!archiveDates.length) { els.archiveList.innerHTML = 'No archived SITREPs yet.'; return; } let html = ''; archiveDates.forEach(entry => { const isActive = entry.date === currentDate ? ' active' : ''; html += `${formatArchiveDate(entry.date)} ${escapeHtml(entry.headline || '—')} ${entry.sources_used || 0} SRC`; }); els.archiveList.innerHTML = html; updateNav(); } catch (e) { console.error('Archive load error:', e); } } // ─── Generate SITREP ───────────────────────────── async function generateSitrep() { if (!els.btnGenerate) return; els.btnGenerate.disabled = true; els.btnGenerate.textContent = 'GENERATING...'; try { const resp = await fetch(`${API_BASE}/generate`, { method: 'POST' }); const data = await resp.json(); if (data.status === 'ok') { els.btnGenerate.textContent = '✓ GENERATED'; // Reload setTimeout(() => { loadSitrep(); loadArchive(); els.btnGenerate.textContent = 'GENERATE NOW'; els.btnGenerate.disabled = false; }, 1000); } else { els.btnGenerate.textContent = '✗ FAILED'; console.error('Generate error:', data); setTimeout(() => { els.btnGenerate.textContent = 'GENERATE NOW'; els.btnGenerate.disabled = false; }, 3000); } } catch (e) { console.error('Generate error:', e); els.btnGenerate.textContent = '✗ ERROR'; setTimeout(() => { els.btnGenerate.textContent = 'GENERATE NOW'; els.btnGenerate.disabled = false; }, 3000); } } // ─── Utility ────────────────────────────────────── function escapeHtml(str) { const div = document.createElement('div'); div.textContent = str; return div.innerHTML; } // ─── Public API (for archive clicks) ───────────── window.__loadSitrep = loadSitrep; // ─── Init ──────────────────────────────────────── function init() { // Load today's SITREP loadSitrep(); // Load archive loadArchive(); // Generate button if (els.btnGenerate) { els.btnGenerate.addEventListener('click', generateSitrep); } } // Run on DOM ready if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); } else { init(); } })();