From 432d758b900ccc0ee324ecce062724d1e77cb815 Mon Sep 17 00:00:00 2001 From: Otto Date: Sun, 31 May 2026 16:27:52 +0200 Subject: [PATCH] feat(dashboard): export current view --- static/style.css | 22 ++++++++++++ templates/dashboard.html | 76 +++++++++++++++++++++++++++++++++++----- 2 files changed, 90 insertions(+), 8 deletions(-) diff --git a/static/style.css b/static/style.css index 157f383..bdbb526 100644 --- a/static/style.css +++ b/static/style.css @@ -216,6 +216,28 @@ body { outline: none; } +#exportFormat { + background: #1e1e28; + border: 1px solid #2a2a3a; + border-radius: 10px; + padding: 10px; + color: #e8e8ee; + font-size: 0.85rem; + outline: none; +} + +.btn-export { + background: #1e1e28; + border: 1px solid #2a2a3a; + border-radius: 10px; + padding: 10px 12px; + color: #cfd3ff; + font-weight: 700; + font-size: 0.85rem; + cursor: pointer; +} +.btn-export:active { transform: scale(0.98); } + /* ─── New Engram ──────────────────────────────────────────────────────────── */ .new-engram { padding: 0 12px 8px; diff --git a/templates/dashboard.html b/templates/dashboard.html index 8c5141f..c967e19 100644 --- a/templates/dashboard.html +++ b/templates/dashboard.html @@ -34,6 +34,11 @@ + + @@ -167,14 +172,7 @@ function setView(view) { } async function loadCards() { - let url = `/api/engrams?limit=${state.limit}&offset=${state.offset}`; - if (state.search) url += `&search=${encodeURIComponent(state.search)}`; - if (state.filter === 'confirmed') url += '&confirmed=1'; - if (state.filter === 'pending') url += '&confirmed=0'; - if (state.filter === 'rejected') url += '&verdict=confirmed_false'; - if (state.filter === 'errors') url += '&tag=error'; - - const data = await api(url); + const data = await api(buildEngramsUrl(state.limit, state.offset)); state.items = data.items; renderCardsWithSuggestions(); document.getElementById('pageNum').textContent = Math.floor(state.offset / state.limit) + 1; @@ -182,6 +180,68 @@ async function loadCards() { document.getElementById('btnNext').disabled = data.items.length < state.limit; } +function buildEngramsUrl(limit, offset) { + let url = `/api/engrams?limit=${limit}&offset=${offset}`; + if (state.search) url += `&search=${encodeURIComponent(state.search)}`; + if (state.filter === 'confirmed') url += '&confirmed=1'; + if (state.filter === 'pending') url += '&confirmed=0'; + if (state.filter === 'rejected') url += '&verdict=confirmed_false'; + if (state.filter === 'errors') url += '&tag=error'; + return url; +} + +async function exportCurrent() { + const fmt = (document.getElementById('exportFormat')?.value || 'jsonl').toLowerCase(); + const limit = 100; + const max = 5000; + let offset = 0; + let all = []; + while (all.length < max) { + const data = await api(buildEngramsUrl(limit, offset)); + const items = data.items || []; + all = all.concat(items); + if (items.length < limit) break; + offset += limit; + } + + const safe = (s) => String(s ?? '').replace(/[\\r\\n]+/g, ' ').trim(); + let payload = ''; + let mime = 'text/plain'; + if (fmt === 'csv') { + mime = 'text/csv'; + const esc = (v) => '\"' + String(v ?? '').replace(/\"/g, '\"\"') + '\"'; + payload += ['id','created','source','confidence','verdict','tags','content'].join(',') + '\\n'; + for (const it of all) { + payload += [ + esc(it.id), + esc(it.created), + esc(it.source), + esc(it.confidence), + esc(it.verdict), + esc((it.tags || []).join('|')), + esc((it.content || '').replace(/\\r?\\n/g, '\\\\n')), + ].join(',') + '\\n'; + } + } else { + mime = 'application/x-ndjson'; + payload = all.map(x => JSON.stringify(x)).join('\\n') + (all.length ? '\\n' : ''); + } + + const now = new Date(); + const ymd = `${now.getFullYear()}-${String(now.getMonth()+1).padStart(2,'0')}-${String(now.getDate()).padStart(2,'0')}`; + const filename = `second-brain_${ymd}_${safe(state.filter || 'all')}_${fmt}.${fmt}`; + + const blob = new Blob([payload], {type: mime}); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = filename; + document.body.appendChild(a); + a.click(); + a.remove(); + setTimeout(() => URL.revokeObjectURL(url), 1000); +} + async function loadStatus() { const reqs = await Promise.allSettled([ api('/api/config'),