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'),