From 0ff6db73ea30c0f21efd628e02fce82aceabd506 Mon Sep 17 00:00:00 2001 From: Otto Date: Sun, 31 May 2026 15:42:46 +0200 Subject: [PATCH 01/10] feat(dashboard): integrate link suggestions and predictive links into UI - FastAPI: parse_engram now includes link_suggestions and predictive_links from metadata - FastAPI: add POST /api/links/accept to create links from suggestions - Dashboard: new renderCardsWithSuggestions() displays suggestions in each card - Dashboard: acceptLink() function handles click-to-link - Dashboard: loadCards() calls renderCardsWithSuggestions() - Systemd: remove DirectoryNotEmpty from ingest path unit (already present) Refs: #25 #27 --- cron_tasks/import_context_buffer.py | 43 ++++++++-------- fastapi_app.py | 33 ++++++++++++- .../openclaw-secondbrain-ingest-memory.path | 1 - templates/dashboard.html | 49 ++++++++++++++++++- 4 files changed, 102 insertions(+), 24 deletions(-) diff --git a/cron_tasks/import_context_buffer.py b/cron_tasks/import_context_buffer.py index c3aeef5..32e97f5 100644 --- a/cron_tasks/import_context_buffer.py +++ b/cron_tasks/import_context_buffer.py @@ -15,34 +15,35 @@ from pathlib import Path BRAIN_DIR = Path("/root/.openclaw/workspace/second-brain") WORKSPACE = Path("/root/.openclaw/workspace") -HANDLER = WORKSPACE / "context-buffer" / "handler.py" +CURRENT_DIR = WORKSPACE / "context-buffer" / "current" def run(): - # Hole alle Topics mit status done/completed via handler + # Lese context-buffer index.json direkt + index_path = WORKSPACE / "context-buffer" / "index.json" try: - result = subprocess.run( - ["python3", str(HANDLER), "search", "--status", "done"], - capture_output=True, text=True, timeout=30 - ) - if result.returncode != 0: - raise Exception(f"Handler error: {result.stderr}") - topics = json.loads(result.stdout) + with open(index_path) as f: + idx = json.load(f) + topics = [] + for tid, t in idx.get("topics", {}).items(): + status = t.get("status", "active") + if status in ("done", "completed"): + # Lade den vollen Inhalt aus der topic-Datei + topic_file = CURRENT_DIR / f"topic-{tid}.md" + if topic_file.exists(): + content = topic_file.read_text(encoding="utf-8") + # Entferne Frontmatter für reinen Content + if content.startswith("---"): + parts = content.split("---", 2) + if len(parts) >= 3: + content = parts[2].strip() + t["content"] = content + else: + t["content"] = "" + topics.append(t) except Exception as e: print(json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False)) return - # Alternative: auch 'completed' suchen - try: - result2 = subprocess.run( - ["python3", str(HANDLER), "search", "--status", "completed"], - capture_output=True, text=True, timeout=30 - ) - if result2.returncode == 0: - topics_completed = json.loads(result2.stdout) - topics.extend(topics_completed) - except Exception: - pass - if not topics: print(json.dumps({"success": True, "imported": 0, "message": "No completed topics found"}, indent=2, ensure_ascii=False)) return diff --git a/fastapi_app.py b/fastapi_app.py index 275efc5..a09ed0c 100644 --- a/fastapi_app.py +++ b/fastapi_app.py @@ -63,7 +63,7 @@ def parse_engram(row: sqlite3.Row) -> dict: verdict = "confirmed_false" else: verdict = "unknown" - return { + result = { "id": row["id"], "content": row["content"], "confidence": meta.get("confidence", 0.0), @@ -81,6 +81,12 @@ def parse_engram(row: sqlite3.Row) -> dict: "access_count": meta.get("access_count", 0), "grounding": meta.get("grounding", 0), } + # Vorschläge aus metadata + if "link_suggestions" in meta: + result["link_suggestions"] = meta["link_suggestions"] + if "predictive_links" in meta: + result["predictive_links"] = meta["predictive_links"] + return result def _now_iso() -> str: @@ -902,6 +908,31 @@ def api_refresh(engram_id: str): return {"success": True, "new_confidence": round(conf, 2)} +@app.post("/api/links/accept") +def api_accept_link(from_id: str = Form(...), to_id: str = Form(...)): + """Erstelle einen Link zwischen zwei Engrammen (aus Vorschlag).""" + conn = get_db() + c = conn.cursor() + # Prüfe Existenz beider Engramme + for eid in (from_id, to_id): + if not c.execute("SELECT 1 FROM engrams WHERE id = ?", (eid,)).fetchone(): + conn.close() + return JSONResponse({"error": f"Engram {eid} not found"}, status_code=404) + # Vermeide Duplikate + c.execute("SELECT 1 FROM engrams_links WHERE from_id = ? AND to_id = ?", (from_id, to_id)) + if c.fetchone(): + conn.close() + return {"ok": True, "message": "link already exists"} + # Link erstellen + c.execute( + "INSERT INTO engrams_links (from_id, to_id) VALUES (?, ?)", + (from_id, to_id) + ) + conn.commit() + conn.close() + return {"ok": True} + + @app.post("/api/engrams") def api_create_engram(content: str = Form(...), tags: str = Form(""), source: str = Form("web")): engram_id = f"web-{datetime.now(timezone.utc).strftime('%Y%m%d-%H%M%S-%f')[:20]}" diff --git a/systemd/openclaw-secondbrain-ingest-memory.path b/systemd/openclaw-secondbrain-ingest-memory.path index 5679b37..d61a188 100644 --- a/systemd/openclaw-secondbrain-ingest-memory.path +++ b/systemd/openclaw-secondbrain-ingest-memory.path @@ -4,7 +4,6 @@ PartOf=openclaw-secondbrain.target [Path] PathModified=/root/.openclaw/workspace/memory -DirectoryNotEmpty=true [Install] WantedBy=multi-user.target \ No newline at end of file diff --git a/templates/dashboard.html b/templates/dashboard.html index daea77d..aad5d7a 100644 --- a/templates/dashboard.html +++ b/templates/dashboard.html @@ -170,7 +170,7 @@ async function loadCards() { const data = await api(url); state.items = data.items; - renderCards(); + renderCardsWithSuggestions(); document.getElementById('pageNum').textContent = Math.floor(state.offset / state.limit) + 1; document.getElementById('btnPrev').disabled = state.offset === 0; document.getElementById('btnNext').disabled = data.items.length < state.limit; @@ -1114,6 +1114,53 @@ setInterval(() => { // ─── Init ─────────────────────────────────────────────────────────────────── loadStats(); loadCards(); +function renderCardsWithSuggestions() { + const el = document.getElementById('cards'); + el.innerHTML = state.items.map(item => { + const suggestions = (item.link_suggestions || []).concat(item.predictive_links || []); + const suggHtml = suggestions.length ? suggestions.map(s => ` +
+ ${s.engram_id.substring(0,8)} + ${escapeHtml(s.preview || s.content_preview || '')} + +
+ `).join('') : 'Keine Vorschläge'; + return ` +
+
+ ${Math.round(item.confidence*100)}% + ${renderVerdictPill(item)} + ${item.tags.map(t => ''+t+'').join('')} + ${fmtDate(item.created)} +
+
+ ${escapeHtml(item.content.substring(0, 200))}${item.content.length>200?'...':''} +
+
+ Vorschläge: ${suggHtml} +
+ +
+ `).join(''); +} + +async function acceptLink(fromId, toId, ev) { + ev.stopPropagation(); + await api('/api/links/accept', { + method: 'POST', + headers: {'Content-Type': 'application/x-www-form-urlencoded'}, + body: new URLSearchParams({from_id: fromId, to_id: toId}) + }); + alert('Link erstellt'); + await loadCards(); +} From 672077a14c9c30680b35bdde8e5011ab693a0a62 Mon Sep 17 00:00:00 2001 From: Otto Date: Sun, 31 May 2026 16:05:53 +0200 Subject: [PATCH 02/10] fix(dashboard): render cards again --- templates/dashboard.html | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/templates/dashboard.html b/templates/dashboard.html index aad5d7a..b8c5654 100644 --- a/templates/dashboard.html +++ b/templates/dashboard.html @@ -1148,7 +1148,8 @@ function renderCardsWithSuggestions() { - `).join(''); + `; + }).join(''); } async function acceptLink(fromId, toId, ev) { From 1db483c0530d6a441d0f01001ce1132e35a3cb52 Mon Sep 17 00:00:00 2001 From: Otto Date: Sun, 31 May 2026 16:24:40 +0200 Subject: [PATCH 03/10] feat(dashboard): extend stats bar --- templates/dashboard.html | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/templates/dashboard.html b/templates/dashboard.html index b8c5654..d7c3c90 100644 --- a/templates/dashboard.html +++ b/templates/dashboard.html @@ -12,8 +12,10 @@
-Total
-OK
+
-Rej
-Pending
-Err
+
-Avg
@@ -132,8 +134,10 @@ async function loadStats() { const s = await api('/api/stats'); document.getElementById('statTotal').textContent = s.total; document.getElementById('statConfirmed').textContent = s.confirmed; + document.getElementById('statRejected').textContent = (s.rejected ?? '-'); document.getElementById('statPending').textContent = s.pending; document.getElementById('statErrors').textContent = s.errors; + document.getElementById('statAvgConf').textContent = (typeof s.avg_confidence === 'number') ? `${Math.round(s.avg_confidence * 100)}%` : '-'; } function updateStatsFromEvent(ev) { @@ -141,8 +145,10 @@ function updateStatsFromEvent(ev) { const s = ev.stats; document.getElementById('statTotal').textContent = s.total; document.getElementById('statConfirmed').textContent = s.confirmed; + if (document.getElementById('statRejected')) document.getElementById('statRejected').textContent = (s.rejected ?? '-'); document.getElementById('statPending').textContent = s.pending; document.getElementById('statErrors').textContent = s.errors; + if (document.getElementById('statAvgConf')) document.getElementById('statAvgConf').textContent = (typeof s.avg_confidence === 'number') ? `${Math.round(s.avg_confidence * 100)}%` : '-'; } function setView(view) { From 788ee1539d874f4eda4c24917b87f29e4271cbf7 Mon Sep 17 00:00:00 2001 From: Otto Date: Sun, 31 May 2026 16:26:37 +0200 Subject: [PATCH 04/10] feat(dashboard): richer engram detail overlay --- templates/dashboard.html | 84 +++++++++++++++++++++++++++++++++------- 1 file changed, 69 insertions(+), 15 deletions(-) diff --git a/templates/dashboard.html b/templates/dashboard.html index d7c3c90..8c5141f 100644 --- a/templates/dashboard.html +++ b/templates/dashboard.html @@ -1007,9 +1007,11 @@ function escapeHtml(t) { } // ─── Actions ──────────────────────────────────────────────────────────────── -async function confirm(id, ev) { +async function confirm(id, ev, ctx = 'card') { ev.stopPropagation(); - const reason = document.getElementById('reason-'+id).value; + const reasonElId = (ctx === 'modal') ? ('reason-modal-' + id) : ('reason-' + id); + const reasonEl = document.getElementById(reasonElId); + const reason = reasonEl ? reasonEl.value : ''; await api(`/api/engrams/${id}/confirm`, { method: 'POST', headers: {'Content-Type': 'application/x-www-form-urlencoded'}, @@ -1020,9 +1022,11 @@ async function confirm(id, ev) { if (state.view === 'status') loadStatus(); } -async function reject(id, ev) { +async function reject(id, ev, ctx = 'card') { ev.stopPropagation(); - const reason = document.getElementById('reason-'+id).value; + const reasonElId = (ctx === 'modal') ? ('reason-modal-' + id) : ('reason-' + id); + const reasonEl = document.getElementById(reasonElId); + const reason = reasonEl ? reasonEl.value : ''; await api(`/api/engrams/${id}/reject`, { method: 'POST', headers: {'Content-Type': 'application/x-www-form-urlencoded'}, @@ -1058,18 +1062,62 @@ async function createEngram() { async function showDetail(id) { const item = await api(`/api/engrams/${id}`); const body = document.getElementById('modalBody'); + const links = (item.links || []); + const suggestions = (item.link_suggestions || []).concat(item.predictive_links || []); + const suggHtml = suggestions.length ? suggestions.map(s => ` +
+ ${s.engram_id.substring(0,8)} + ${escapeHtml(s.preview || s.content_preview || '')} + +
+ `).join('') : 'Keine Vorschläge'; body.innerHTML = ` -

Engramm ${item.id.substring(0,8)}

-

Confidence: ${Math.round(item.confidence*100)}%

-

Confirmed: ${item.confirmed ? '✅' : '❌'}

-

Tags: ${item.tags.map(t => ''+t+'').join(' ')}

-

Content:

-
${escapeHtml(item.content)}
-

History:

-
    - ${(item.review_history || []).map(h => `
  • ${fmtDate(h.at)} — ${h.action} (${h.note})
  • `).join('')} -
-

Links: ${item.links?.join(', ') || 'none'}

+

Engramm ${item.id.substring(0,8)}

+
verdict
${renderVerdictPill(item)} ${Math.round(item.confidence*100)}%
+
source
${escapeHtml(item.source || '-')}
+
created
${fmtDate(item.created)}
+
modified
${fmtDate(item.modified)}
+
access
${item.access_count ?? 0} • grounding ${item.grounding ?? 0}
+
tags
${(item.tags || []).map(t => ''+escapeHtml(t)+'').join(' ') || '-'}
+ +
Content
+
${escapeHtml(item.content || '')}
+ +
+
Vorschläge
+
${suggHtml}
+
+ +
+
Links
+
+ ${links.length ? links.map(l => `${l.substring(0,8)}`).join(' ') : 'none'} +
+
+ + ${Array.isArray(item.evidence) && item.evidence.length ? ` +
+
Evidence
+
    + ${item.evidence.map(e => `
  • ${escapeHtml(typeof e === 'string' ? e : JSON.stringify(e))}
  • `).join('')} +
+
` : ''} + +
+
History
+
    + ${(item.review_history || []).map(h => `
  • ${fmtDate(h.at)} — ${escapeHtml(h.action)} ${h.note ? ('(' + escapeHtml(h.note) + ')') : ''}
  • `).join('') || '
  • -
  • '} +
+
+ + `; document.getElementById('detailModal').classList.add('open'); } @@ -1120,6 +1168,12 @@ setInterval(() => { // ─── Init ─────────────────────────────────────────────────────────────────── loadStats(); loadCards(); +document.getElementById('detailModal').addEventListener('click', (e) => { + if (e.target && e.target.id === 'detailModal') closeModal(); +}); +document.addEventListener('keydown', (e) => { + if (e.key === 'Escape') closeModal(); +}); function renderCardsWithSuggestions() { const el = document.getElementById('cards'); el.innerHTML = state.items.map(item => { From 432d758b900ccc0ee324ecce062724d1e77cb815 Mon Sep 17 00:00:00 2001 From: Otto Date: Sun, 31 May 2026 16:27:52 +0200 Subject: [PATCH 05/10] 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'), From f8de7e626b43c19d8568f593d6a22901ff8a73a5 Mon Sep 17 00:00:00 2001 From: Otto Date: Sun, 31 May 2026 16:29:15 +0200 Subject: [PATCH 06/10] feat(dashboard): keyboard navigation --- static/style.css | 4 ++++ templates/dashboard.html | 44 +++++++++++++++++++++++++++++++++++++++- 2 files changed, 47 insertions(+), 1 deletion(-) diff --git a/static/style.css b/static/style.css index bdbb526..973ea74 100644 --- a/static/style.css +++ b/static/style.css @@ -288,6 +288,10 @@ body { transition: transform 0.15s ease, border-color 0.2s ease; touch-action: manipulation; } +.card.selected { + border-color: #6c8af5; + box-shadow: 0 0 0 1px rgba(108,138,245,0.25) inset; +} .card:active { transform: scale(0.985); } .card.confirmed { border-left: 4px solid #3a7d3a; } .card.rejected { border-left: 4px solid #8a3a3a; } diff --git a/templates/dashboard.html b/templates/dashboard.html index c967e19..49f5e2b 100644 --- a/templates/dashboard.html +++ b/templates/dashboard.html @@ -115,6 +115,7 @@ let state = { autoRefresh: true, view: 'cards', lastEvent: null, + selectedId: null, }; // ─── Fetch ────────────────────────────────────────────────────────────────── @@ -1186,6 +1187,15 @@ function closeModal() { document.getElementById('detailModal').classList.remove('open'); } +function selectCard(id) { + state.selectedId = id; + renderCardsWithSuggestions(); + setTimeout(() => { + const el = document.querySelector(`.card[data-id=\"${id}\"]`); + if (el) el.scrollIntoView({block: 'nearest', behavior: 'smooth'}); + }, 0); +} + // ─── Pagination ───────────────────────────────────────────────────────────── function nextPage() { state.offset += state.limit; @@ -1233,6 +1243,38 @@ document.getElementById('detailModal').addEventListener('click', (e) => { }); document.addEventListener('keydown', (e) => { if (e.key === 'Escape') closeModal(); + if (e.key === '/' && !(e.target && (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA' || e.target.tagName === 'SELECT'))) { + e.preventDefault(); + document.getElementById('searchInput')?.focus(); + } + + if (e.target && (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA' || e.target.tagName === 'SELECT')) return; + if (document.getElementById('detailModal')?.classList.contains('open')) return; + + if (e.key === 'g') setView('graph'); + if (e.key === 's') setView('status'); + if (e.key === '1') setView('cards'); + + if (state.view !== 'cards') return; + if (!state.items || !state.items.length) return; + + const idxOf = (id) => state.items.findIndex(x => x.id === id); + let idx = state.selectedId ? idxOf(state.selectedId) : -1; + if (idx < 0) idx = 0; + + if (e.key === 'j') { + idx = Math.min(state.items.length - 1, idx + 1); + selectCard(state.items[idx].id); + } else if (e.key === 'k') { + idx = Math.max(0, idx - 1); + selectCard(state.items[idx].id); + } else if (e.key === 'Enter') { + showDetail(state.items[idx].id); + } else if (e.key === 'c') { + confirm(state.items[idx].id, e); + } else if (e.key === 'r') { + reject(state.items[idx].id, e); + } }); function renderCardsWithSuggestions() { const el = document.getElementById('cards'); @@ -1246,7 +1288,7 @@ function renderCardsWithSuggestions() { `).join('') : 'Keine Vorschläge'; return ` -
+
${Math.round(item.confidence*100)}% ${renderVerdictPill(item)} From 9ec1e0d28f5394d723eb66bcf3e6118ae9ed9c6b Mon Sep 17 00:00:00 2001 From: Otto Date: Sun, 31 May 2026 16:29:46 +0200 Subject: [PATCH 07/10] style(dashboard): improve small-screen layout --- static/style.css | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/static/style.css b/static/style.css index 973ea74..368f74c 100644 --- a/static/style.css +++ b/static/style.css @@ -481,3 +481,16 @@ body { @media (pointer: coarse) { button, .card { -webkit-tap-highlight-color: transparent; } } + +/* ─── Small Screens ────────────────────────────────────────────────────────── */ +@media (max-width: 420px) { + html { font-size: 13px; } + .stat { min-width: 48px; } + .stat-num { font-size: 1.15rem; } + .tabs-bar { top: 48px; } + .search-box { flex-wrap: wrap; } + #searchInput { flex: 1 1 100%; } + #filterSelect, #exportFormat, .btn-export { flex: 1 1 calc(33% - 6px); } + .modal { padding: 14px 10px; } + .modal-content { padding: 16px 12px; } +} From c22c7be44454e9c1739d9f0d540a9fb3e38678de Mon Sep 17 00:00:00 2001 From: Otto Date: Sun, 31 May 2026 16:45:18 +0200 Subject: [PATCH 08/10] fix(dashboard): mobile viewport and search bar overflow --- static/style.css | 6 ++++++ templates/dashboard.html | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/static/style.css b/static/style.css index 368f74c..b316bd0 100644 --- a/static/style.css +++ b/static/style.css @@ -14,6 +14,7 @@ body { margin: 0 auto; min-height: 100vh; background: #141419; + width: 100%; } /* ─── Stats Bar ───────────────────────────────────────────────────────────── */ @@ -78,6 +79,7 @@ body { gap: 8px; padding: 10px 12px; background: #141419; + flex-wrap: wrap; } /* tab buttons styled via .tabs-bar */ @@ -197,6 +199,7 @@ body { .graph-hint{ padding: 4px 12px 10px; } #searchInput { flex: 1; + min-width: 0; background: #1e1e28; border: 1px solid #2a2a3a; border-radius: 10px; @@ -214,6 +217,7 @@ body { color: #e8e8ee; font-size: 0.85rem; outline: none; + min-width: 0; } #exportFormat { @@ -224,6 +228,7 @@ body { color: #e8e8ee; font-size: 0.85rem; outline: none; + min-width: 0; } .btn-export { @@ -235,6 +240,7 @@ body { font-weight: 700; font-size: 0.85rem; cursor: pointer; + min-width: 0; } .btn-export:active { transform: scale(0.98); } diff --git a/templates/dashboard.html b/templates/dashboard.html index 49f5e2b..541f0a0 100644 --- a/templates/dashboard.html +++ b/templates/dashboard.html @@ -2,7 +2,7 @@ - + 🧠 Second Brain From dab1b84a6849712e73c08767536966bd29baa960 Mon Sep 17 00:00:00 2001 From: Otto Date: Sun, 31 May 2026 17:00:42 +0200 Subject: [PATCH 09/10] =?UTF-8?q?fix(dashboard):=20site-only=20layout=20?= =?UTF-8?q?=E2=80=94=20suchfeld=20oben,=20filter/export=20unten=20auf=20mo?= =?UTF-8?q?bil?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- static/style.css | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/static/style.css b/static/style.css index b316bd0..e7b1554 100644 --- a/static/style.css +++ b/static/style.css @@ -495,8 +495,8 @@ body { .stat-num { font-size: 1.15rem; } .tabs-bar { top: 48px; } .search-box { flex-wrap: wrap; } - #searchInput { flex: 1 1 100%; } - #filterSelect, #exportFormat, .btn-export { flex: 1 1 calc(33% - 6px); } + #searchInput { flex: 1 0 100%; min-width: 0; } + #filterSelect, #exportFormat, .btn-export { flex: 1 1 auto; min-width: 0; } .modal { padding: 14px 10px; } .modal-content { padding: 16px 12px; } } From 680b3869bb07da8d7745652b45f144b14ef7ead5 Mon Sep 17 00:00:00 2001 From: Otto Date: Sun, 31 May 2026 17:09:24 +0200 Subject: [PATCH 10/10] =?UTF-8?q?fix(dashboard):=20explizite=20zwei-zeilen?= =?UTF-8?q?-layout=20f=C3=BCr=20suchbox?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- static/style.css | 26 +++++++++++++++++++------- templates/dashboard.html | 30 +++++++++++++++++------------- 2 files changed, 36 insertions(+), 20 deletions(-) diff --git a/static/style.css b/static/style.css index e7b1554..8ae430d 100644 --- a/static/style.css +++ b/static/style.css @@ -76,10 +76,21 @@ body { /* ─── Search ──────────────────────────────────────────────────────────────── */ .search-box { display: flex; + flex-direction: column; gap: 8px; padding: 10px 12px; background: #141419; - flex-wrap: wrap; +} +.search-row { + display: flex; + gap: 8px; + flex-direction: row; +} +.search-row:first-child { + width: 100%; +} +.search-row:last-child { + width: 100%; } /* tab buttons styled via .tabs-bar */ @@ -198,6 +209,7 @@ body { .legend-dot.match{ background:#f7d154; } .graph-hint{ padding: 4px 12px 10px; } #searchInput { + width: 100%; flex: 1; min-width: 0; background: #1e1e28; @@ -210,6 +222,8 @@ body { } #searchInput:focus { border-color: #6c8af5; } #filterSelect { + flex: 1; + min-width: 0; background: #1e1e28; border: 1px solid #2a2a3a; border-radius: 10px; @@ -217,10 +231,11 @@ body { color: #e8e8ee; font-size: 0.85rem; outline: none; - min-width: 0; } #exportFormat { + flex: 1; + min-width: 0; background: #1e1e28; border: 1px solid #2a2a3a; border-radius: 10px; @@ -228,10 +243,11 @@ body { color: #e8e8ee; font-size: 0.85rem; outline: none; - min-width: 0; } .btn-export { + flex: 1; + min-width: 0; background: #1e1e28; border: 1px solid #2a2a3a; border-radius: 10px; @@ -240,7 +256,6 @@ body { font-weight: 700; font-size: 0.85rem; cursor: pointer; - min-width: 0; } .btn-export:active { transform: scale(0.98); } @@ -494,9 +509,6 @@ body { .stat { min-width: 48px; } .stat-num { font-size: 1.15rem; } .tabs-bar { top: 48px; } - .search-box { flex-wrap: wrap; } - #searchInput { flex: 1 0 100%; min-width: 0; } - #filterSelect, #exportFormat, .btn-export { flex: 1 1 auto; min-width: 0; } .modal { padding: 14px 10px; } .modal-content { padding: 16px 12px; } } diff --git a/templates/dashboard.html b/templates/dashboard.html index 541f0a0..221466c 100644 --- a/templates/dashboard.html +++ b/templates/dashboard.html @@ -26,19 +26,23 @@