|
|
@@ -8,7 +8,7 @@ var _feedsData = [];
|
|
|
var _healthLoaded = false;
|
|
|
|
|
|
function switchView(name) {
|
|
|
- var views = ['health','feeds','clusters','sentiment','entities','keywords','detail'];
|
|
|
+ var views = ['health','feeds','clusters','sentiment','entities','keywords','config'];
|
|
|
if (views.indexOf(name) === -1) return;
|
|
|
document.querySelectorAll('.view').forEach(function(v) { v.classList.toggle('active', v.id === 'view-' + name); });
|
|
|
document.querySelectorAll('.nav-links a').forEach(function(a) { a.classList.toggle('active', a.dataset.view === name); });
|
|
|
@@ -18,6 +18,7 @@ function switchView(name) {
|
|
|
if (name === 'sentiment') reloadSentiment();
|
|
|
if (name === 'entities') loadEntities();
|
|
|
if (name === 'keywords') loadKeywords();
|
|
|
+ if (name === 'config') loadConfig();
|
|
|
}
|
|
|
|
|
|
function $(id) {
|
|
|
@@ -466,35 +467,109 @@ function openClusterModal(clusterId) {
|
|
|
|
|
|
function closeModal() { var m = $('cluster-modal'); if (m) m.classList.remove('open'); }
|
|
|
|
|
|
-function searchDetail() {
|
|
|
- var q = $('detail-search').value.trim();
|
|
|
- if (!q) return;
|
|
|
- var el = $('detail-content'); if (el) el.innerHTML = '<div class="loading">Searching...</div>';
|
|
|
- fetch(API + '/cluster/' + encodeURIComponent(q))
|
|
|
- .then(function(r) { if (r.status===404) { var el2=$('detail-content'); if(el2) el2.innerHTML='<p class="muted">Not found.</p>'; return null; } return r.json(); })
|
|
|
- .then(function(d) { if (d) { var el3=$('detail-content'); if(el3) el3.innerHTML = buildDetailHTML(d); } });
|
|
|
-}
|
|
|
-
|
|
|
-function buildDetailHTML(d) {
|
|
|
- var h = '<div class="detail-section"><h4>Headline</h4><div class="detail-content">' + esc(d.headline||'') + '</div></div>';
|
|
|
- h += '<div class="detail-section"><h4>Metadata</h4><div class="detail-content">';
|
|
|
- h += topicChip(d.topic) + ' <span class="' + sentimentClass(d.sentiment) + '" style="font-weight:600">' + esc(d.sentiment) + ' (' + esc(String(d.sentimentScore||'')) + ')</span>';
|
|
|
- h += ' <span style="color:var(--accent);margin-left:.5rem">Importance: ' + (d.importance||0) + '</span>';
|
|
|
- h += '<br><span class="muted">First: ' + (d.first_seen||'n/a') + ' · Updated: ' + (d.last_updated||'n/a') + '</span></div></div>';
|
|
|
- if (d.summary_text) h += '<div class="detail-section"><h4>Summary</h4><div class="detail-content">' + esc(d.summary_text) + '</div></div>';
|
|
|
- if (d.summary) h += '<div class="detail-section"><h4>LLM Summary</h4><div class="detail-content">' + esc(d.summary) + '</div></div>';
|
|
|
- if (d.key_facts && d.key_facts.length) h += '<div class="detail-section"><h4>Key Facts</h4><div class="detail-content">' + d.key_facts.map(function(f){return '<div class="key-fact">• ' + esc(f) + '</div>';}).join('') + '</div></div>';
|
|
|
- if (d.entities && d.entities.length) h += '<div class="detail-section"><h4>Entities ('+d.entities.length+')</h4><div class="detail-content">' + d.entities.map(function(e){return '<span class="chip">' + esc(e) + '</span>';}).join('') + '</div></div>';
|
|
|
- if (d.keywords && d.keywords.length) h += '<div class="detail-section"><h4>Keywords ('+d.keywords.length+')</h4><div class="detail-content">' + d.keywords.map(function(k){return '<span class="chip">' + esc(k) + '</span>';}).join('') + '</div></div>';
|
|
|
- if (d.articles && d.articles.length) {
|
|
|
- h += '<div class="detail-section"><h4>Articles ('+d.articles.length+')</h4>';
|
|
|
- for (var ai = 0; ai < Math.min(d.articles.length, 8); ai++) {
|
|
|
- var a = d.articles[ai];
|
|
|
- h += '<div style="margin-bottom:.5rem;padding:.5rem;background:var(--surface2);border-radius:6px;font-size:.82rem"><a href="' + esc(a.url||'#') + '" target="_blank" rel="noopener">' + esc(a.title||'Untitled') + '</a><br><span class="muted">' + esc(a.source||'') + ' · ' + (a.timestamp||'') + '</span></div>';
|
|
|
+// ── Config ──────────────────────────────────────────────
|
|
|
+
|
|
|
+async function loadConfig() {
|
|
|
+ var el = $('config-list');
|
|
|
+ if (!el) return;
|
|
|
+ el.innerHTML = '<div class="loading">Loading…</div>';
|
|
|
+ try {
|
|
|
+ var res = await fetch(API + '/config');
|
|
|
+ var d = await res.json();
|
|
|
+ renderConfig(d.config || []);
|
|
|
+ } catch(e) {
|
|
|
+ console.error('Config load error:', e);
|
|
|
+ el.innerHTML = '<div class="loading" style="color:var(--red)">Error: ' + esc(e.message) + '</div>';
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+function renderConfig(rows) {
|
|
|
+ var el = $('config-list');
|
|
|
+ if (!el) return;
|
|
|
+ if (!rows.length) {
|
|
|
+ el.innerHTML = '<p class="muted">No config rows found.</p>';
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ // Group by category
|
|
|
+ var cats = {};
|
|
|
+ for (var i = 0; i < rows.length; i++) {
|
|
|
+ var c = rows[i].category || 'other';
|
|
|
+ if (!cats[c]) cats[c] = [];
|
|
|
+ cats[c].push(rows[i]);
|
|
|
+ }
|
|
|
+ var catOrder = ['clustering', 'enrichment', 'retention'];
|
|
|
+ var catLabels = { clustering: '🔗 Clustering', enrichment: '🧠 Enrichment / LLM', retention: '🗄️ Retention / Polling' };
|
|
|
+ var html = '';
|
|
|
+ for (var ci = 0; ci < catOrder.length; ci++) {
|
|
|
+ var cat = catOrder[ci];
|
|
|
+ if (!cats[cat]) continue;
|
|
|
+ html += '<div class="config-section">';
|
|
|
+ html += '<h4 style="margin:.75rem .5rem .5rem;font-size:.85rem;text-transform:uppercase;letter-spacing:.04em;color:var(--text-dim)">' + (catLabels[cat] || esc(cat)) + '</h4>';
|
|
|
+ html += '<div class="config-grid">';
|
|
|
+ for (var j = 0; j < cats[cat].length; j++) {
|
|
|
+ var r = cats[cat][j];
|
|
|
+ var srcBadge = r.source === 'env'
|
|
|
+ ? '<span class="badge" style="background:#1e3a5f;color:#93c5fd;font-size:.65rem;margin-left:.3rem">env</span>'
|
|
|
+ : (r.source === 'api'
|
|
|
+ ? '<span class="badge" style="background:#3a1e5f;color:#c4b5fd;font-size:.65rem;margin-left:.3rem">api</span>'
|
|
|
+ : '<span class="badge" style="background:#1a2e1a;color:#86efac;font-size:.65rem;margin-left:.3rem">default</span>');
|
|
|
+ var id = 'cfg-' + esc(r.key);
|
|
|
+ var desc = esc(r.description || '');
|
|
|
+ html += '<div class="config-row" title="' + desc + '">';
|
|
|
+ html += '<div class="config-key">' + esc(r.key) + srcBadge + '</div>';
|
|
|
+ html += '<div class="config-desc">' + desc + '</div>';
|
|
|
+ if (r.type === 'bool') {
|
|
|
+ html += '<select id="' + id + '" data-key="' + esc(r.key) + '">';
|
|
|
+ html += '<option value="true"' + (r.value === 'true' ? ' selected' : '') + '>true</option>';
|
|
|
+ html += '<option value="false"' + (r.value === 'false' ? ' selected' : '') + '>false</option>';
|
|
|
+ html += '</select>';
|
|
|
+ } else {
|
|
|
+ html += '<input type="' + (r.type === 'int' || r.type === 'float' ? 'number' : 'text') + '" id="' + id + '" value="' + esc(r.value) + '" data-key="' + esc(r.key) + '"' + (r.type === 'float' ? ' step="0.001"' : '') + ' />';
|
|
|
+ }
|
|
|
+ html += '</div>';
|
|
|
+ }
|
|
|
+ html += '</div></div>';
|
|
|
+ }
|
|
|
+ el.innerHTML = html;
|
|
|
+ // Attach event listeners
|
|
|
+ el.querySelectorAll('[data-key]').forEach(function(el) {
|
|
|
+ el.addEventListener('change', function() {
|
|
|
+ updateConfig(this.dataset.key, this.value);
|
|
|
+ });
|
|
|
+ });
|
|
|
+}
|
|
|
+
|
|
|
+async function updateConfig(key, value) {
|
|
|
+ try {
|
|
|
+ var form = new FormData();
|
|
|
+ form.append('key', key);
|
|
|
+ form.append('value', value);
|
|
|
+ var res = await fetch(API + '/config/update', { method: 'POST', body: form });
|
|
|
+ var d = await res.json();
|
|
|
+ if (d.ok) {
|
|
|
+ showToast('Updated ' + key);
|
|
|
+ } else {
|
|
|
+ showToast(d.error || 'Error updating ' + key, true);
|
|
|
}
|
|
|
- h += '</div>';
|
|
|
+ } catch(e) {
|
|
|
+ showToast('Error: ' + e.message, true);
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+async function resetConfig() {
|
|
|
+ if (!confirm('Reset all config values to .env/defaults?')) return;
|
|
|
+ try {
|
|
|
+ var res = await fetch(API + '/config/reset', { method: 'POST' });
|
|
|
+ var d = await res.json();
|
|
|
+ if (d.ok) {
|
|
|
+ showToast('Config reset to defaults');
|
|
|
+ loadConfig();
|
|
|
+ } else {
|
|
|
+ showToast(d.error || 'Reset failed', true);
|
|
|
+ }
|
|
|
+ } catch(e) {
|
|
|
+ showToast('Error: ' + e.message, true);
|
|
|
}
|
|
|
- return h;
|
|
|
}
|
|
|
|
|
|
// ── Toast ──────────────────────────────────────────────────
|