fix(cron): Session-Takeover-Workaround, mobil-Dashboard, Import-Fix standalone

- feat(isolation): Cron-Wrapper (subprocess, /workspace/cron_tasks)
- fix(dashboard): mobil-optimierte Karten statt Tabelle
- fix(imports): sys.path + venv auto-activation
- chore: Lasse flüchtiges /tmp hinter mir

Closes-Bug: Session-Takeover bei isolated Cron
Engramme-bestaetigt: Standalone-Import-OK, Wrapper-getestet
This commit is contained in:
2026-05-25 22:08:52 +02:00
parent 4e0f5e7e9a
commit a5d5b2f2ec
3 changed files with 122 additions and 81 deletions

View File

@@ -1,8 +1,14 @@
"""Second Brain - Gedächtnissystem für OpenClaw."""
from .engram import Engram, Grounding, Correctness, ReviewEntry
from .store import EngramStore
from .retriever import Retriever
try:
from .engram import Engram, Grounding, Correctness, ReviewEntry
from .store import EngramStore
from .retriever import Retriever
except ImportError:
# Fallback: ChromaDB optional, SQLite-core funktioniert immer
from .engram import Engram, Grounding, Correctness, ReviewEntry
from .store import EngramStore
Retriever = None
__version__ = "0.1.0"
__all__ = ["Engram", "Grounding", "Correctness", "ReviewEntry", "EngramStore", "Retriever"]

View File

@@ -13,12 +13,23 @@ import argparse
import http.server
import socketserver
import webbrowser
import sys
from pathlib import Path
from uuid import UUID
from .store import EngramStore
from .engram import Engram, Grounding
from .retriever import Retriever
# Insert project root so `python3 src/dashboard.py` works without `-m`
project_root = str(Path(__file__).parent.parent)
if project_root not in sys.path:
sys.path.insert(0, project_root)
from src.store import EngramStore
from src.engram import Engram, Grounding
# Retriever: optional im venv verfügbar, sonst Fallback
try:
from src.retriever import Retriever
except ImportError:
Retriever = None
DB_PATH = Path(__file__).parent.parent / "data" / "brain.sqlite"
HTML_PATH = Path(__file__).parent.parent / "data" / "dashboard.html"
@@ -27,9 +38,29 @@ HTML_PATH = Path(__file__).parent.parent / "data" / "dashboard.html"
def generate_dashboard() -> str:
"""Generiert HTML-Dashboard aus aktuellem Brain-Stand."""
store = EngramStore(str(DB_PATH))
egs = store.get_all(limit=100)
# Stats: mit Retriever (venv) oder manuell berechnen
if Retriever:
ret = Retriever(store)
stats = ret.stats()
egs = store.get_all(limit=100)
errors = store.search_tag("error", limit=100)
stats["errors"] = len(errors)
else:
from src.engram import Correctness
all_egs = store.get_all(limit=10000)
confirmed = sum(1 for e in all_egs if e.correctness.confirmed)
errors = store.search_tag("error", limit=100)
confidences = [e.compute_confidence() for e in all_egs]
stats = {
"total_engrams": len(all_egs),
"confirmed": confirmed,
"unconfirmed": len(all_egs) - confirmed,
"sources": {},
"db_size_bytes": 0,
"avg_confidence": sum(confidences) / len(confidences) if confidences else 0.0,
"errors": len(errors),
}
# Farbe nach Confidence
def color(conf):
@@ -48,95 +79,81 @@ def generate_dashboard() -> str:
except:
return "UNKNOWN"
# Liste der Engramme
rows = []
# Karten-Ansicht für Mobil
card_rows = []
for eg in egs:
conf = eg.compute_confidence()
rows.append(f"""
<tr>
<td><span style="color:{color(conf)};font-size:1.2em">{marker(conf)}</span></td>
<td><code>{str(eg.id)[:8]}</code></td>
<td>{eg.content[:100]}{'...' if len(eg.content) > 100 else ''}</td>
<td><span class="badge">{grounding_name(eg.metadata.get('grounding', 0))}</span></td>
<td><span class="badge">{eg.metadata.get('source', '?')}</span></td>
<td>{conf:.2f}</td>
<td>{eg.correctness.confirmations}/{eg.correctness.rejections}</td>
<td>{eg.metadata.get('access_count', 0)}</td>
<td>{', '.join(eg.metadata.get('tags', [])[:3])}</td>
</tr>
card_rows.append(f"""
<div class="card">
<div class="card-header">
<span>{marker(conf)} {str(eg.id)[:8]}</span>
<span class="badge" style="background:rgba({int((1-conf)*200+55)},{int(conf*200+55)},100,0.3)">{conf:.2f}</span>
</div>
<div class="card-content">{eg.content[:150]}{'...' if len(eg.content) > 150 else ''}</div>
<div class="card-meta">
<span class="badge">{grounding_name(eg.metadata.get('grounding', 0))}</span>
<span class="badge">{eg.metadata.get('source', '?')}</span>
<span class="badge">✓{eg.correctness.confirmations}/✗{eg.correctness.rejections}</span>
{' '.join([f'<span class="badge">{t}</span>' for t in eg.metadata.get('tags', [])[:3]])}
</div>
</div>
""")
html = f"""<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>🧠 Second Brain Dashboard</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>🧠 Second Brain</title>
<style>
:root {{ --bg: #1a1a2e; --card: #16213e; --text: #eee; --accent: #0f4c75; }}
body {{ font-family: -apple-system, BlinkMacSystemFont, sans-serif; background: var(--bg); color: var(--text); margin: 0; padding: 20px; }}
h1 {{ margin: 0 0 10px; font-size: 1.8em; }}
.stats {{ display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 15px; margin-bottom: 25px; }}
.stat-card {{ background: var(--card); border-radius: 12px; padding: 15px; text-align: center; }}
.stat-card .num {{ font-size: 2em; font-weight: bold; color: #4fc3f7; }}
.stat-card .lbl {{ font-size: 0.85em; opacity: 0.7; }}
table {{ width: 100%; border-collapse: collapse; background: var(--card); border-radius: 12px; overflow: hidden; }}
th {{ text-align: left; padding: 10px; background: var(--accent); font-size: 0.85em; text-transform: uppercase; }}
td {{ padding: 8px 10px; border-bottom: 1px solid rgba(255,255,255,0.05); font-size: 0.9em; vertical-align: top; }}
tr:hover td {{ background: rgba(255,255,255,0.03); }}
.badge {{ display: inline-block; padding: 2px 8px; border-radius: 12px; background: rgba(79,195,247,0.2); font-size: 0.75em; }}
.search {{ margin-bottom: 20px; }}
.search input {{ width: 100%; max-width: 400px; padding: 10px 15px; border-radius: 8px; border: 1px solid rgba(255,255,255,0.1); background: var(--card); color: var(--text); font-size: 1em; }}
.refresh {{ position: fixed; top: 20px; right: 20px; background: #2ecc71; color: #000; border: none; padding: 10px 20px; border-radius: 8px; cursor: pointer; font-weight: bold; }}
.footer {{ margin-top: 30px; text-align: center; opacity: 0.5; font-size: 0.8em; }}
body {{ font-family: -apple-system, BlinkMacSystemFont, sans-serif; background: var(--bg); color: var(--text); margin: 0; padding: 10px; font-size: 14px; }}
h1 {{ margin: 0 0 8px; font-size: 1.3em; text-align: center; }}
.stats {{ display: grid; grid-template-columns: repeat(2, 1fr); gap: 8px; margin-bottom: 15px; }}
.stat-card {{ background: var(--card); border-radius: 10px; padding: 10px; text-align: center; }}
.stat-card .num {{ font-size: 1.5em; font-weight: bold; color: #4fc3f7; }}
.stat-card .lbl {{ font-size: 0.75em; opacity: 0.7; }}
.search {{ margin-bottom: 10px; }}
.search input {{ width: 100%; box-sizing: border-box; padding: 12px; border-radius: 8px; border: 1px solid rgba(255,255,255,0.1); background: var(--card); color: var(--text); font-size: 16px; }}
.card {{ background: var(--card); border-radius: 12px; padding: 12px; margin-bottom: 10px; }}
.card-header {{ display: flex; justify-content: space-between; align-items: center; margin-bottom: 6px; }}
.card-id {{ font-size: 0.75em; opacity: 0.6; }}
.card-content {{ font-size: 0.9em; line-height: 1.4; margin-bottom: 8px; word-break: break-word; }}
.card-meta {{ display: flex; flex-wrap: wrap; gap: 6px; font-size: 0.75em; }}
.badge {{ display: inline-block; padding: 3px 8px; border-radius: 12px; background: rgba(79,195,247,0.2); }}
.refresh {{ position: fixed; bottom: 15px; right: 15px; background: #2ecc71; color: #000; border: none; padding: 12px 16px; border-radius: 50%; cursor: pointer; font-weight: bold; font-size: 1.2em; box-shadow: 0 2px 8px rgba(0,0,0,0.4); }}
.footer {{ margin-top: 20px; text-align: center; opacity: 0.5; font-size: 0.7em; padding-bottom: 60px; }}
</style>
</head>
<body>
<h1>🧠 Second Brain Dashboard</h1>
<button class="refresh" onclick="location.reload()">🔄 Aktualisieren</button>
<h1>🧠 Second Brain</h1>
<button class="refresh" onclick="location.reload()"></button>
<div class="stats">
<div class="stat-card"><div class="num">{stats['total_engrams']}</div><div class="lbl">Engramme</div></div>
<div class="stat-card"><div class="num">{stats['confirmed']}</div><div class="lbl">Bestätigt</div></div>
<div class="stat-card"><div class="num">{stats['unconfirmed']}</div><div class="lbl">Unbestätigt</div></div>
<div class="stat-card"><div class="num">{len(stats.get('sources', dict()))}</div><div class="lbl">Quellen</div></div>
<div class="stat-card"><div class="num">{stats['db_size_bytes']/1024:.0f}</div><div class="lbl">KB Größe</div></div>
<div class="stat-card"><div class="num">{stats['errors']}</div><div class="lbl">Fehler</div></div>
</div>
<div class="search">
<input type="text" placeholder="🔍 Suche nach Engrammen..." id="searchInput" onkeyup="filterTable()">
<input type="text" placeholder="🔍 Suche..." id="searchInput" onkeyup="filterCards()">
</div>
<table id="engramTable">
<thead>
<tr>
<th>Status</th>
<th>ID</th>
<th>Inhalt</th>
<th>Grounding</th>
<th>Quelle</th>
<th>Confidence</th>
<th>Feedback</th>
<th>Zugriffe</th>
<th>Tags</th>
</tr>
</thead>
<tbody>
{''.join(rows)}
</tbody>
</table>
<div id="cards">
{''.join(card_rows)}
</div>
<div class="footer">Generated {__import__('datetime').datetime.now().isoformat()}</div>
<div class="footer">Generated {__import__('datetime').datetime.now().strftime('%H:%M')}</div>
<script>
function filterTable() {{
function filterCards() {{
var input = document.getElementById('searchInput');
var filter = input.value.toLowerCase();
var table = document.getElementById('engramTable');
var rows = table.getElementsByTagName('tr');
for (var i=1; i<rows.length; i++) {{
var txt = rows[i].textContent || rows[i].innerText;
rows[i].style.display = txt.toLowerCase().indexOf(filter) > -1 ? '' : 'none';
var cards = document.getElementById('cards').getElementsByClassName('card');
for (var i=0; i<cards.length; i++) {{
var txt = cards[i].textContent || cards[i].innerText;
cards[i].style.display = txt.toLowerCase().indexOf(filter) > -1 ? '' : 'none';
}}
}}
</script>

View File

@@ -16,10 +16,27 @@ from pathlib import Path
from datetime import datetime, timezone
from typing import Optional, List, Dict, Any
# Ensure project root is on sys.path for standalone usage
project_root = str(Path(__file__).parent.parent)
if project_root not in sys.path:
sys.path.insert(0, project_root)
# Activate virtualenv if available (for chromadb etc.)
venv_path = Path(__file__).parent.parent / ".venv"
if venv_path.exists():
venv_site_packages = list((venv_path / "lib").glob("python3.*/site-packages"))
if venv_site_packages and str(venv_site_packages[0]) not in sys.path:
sys.path.insert(0, str(venv_site_packages[0]))
# Second Brain Import
from .store import EngramStore
from .engram import Engram, Grounding
from .retriever import Retriever
from src.engram import Engram, Grounding
from src.store import EngramStore
# Retriever: optional (braucht chromadb)
try:
from src.retriever import Retriever
except ImportError:
Retriever = None
# --- Konfiguration ---
@@ -75,11 +92,6 @@ def heartbeat_check() -> Optional[str]:
Rückgabe: Nachricht für den User, oder None wenn nichts zu tun.
"""
store = get_brain()
ret = Retriever(store)
# A: Unbestätigte Engramme die seit längerem nicht geprüft wurden
# B: Hohe-Prioritäts-Themen (tags wie "wichtig", "dringend")
# C: Fehler-Engramme die repeating sind
# Prüfe auf wichtige unbestätigte Engramme
egs = store.get_all(limit=50)
@@ -89,8 +101,7 @@ def heartbeat_check() -> Optional[str]:
][:5]
if unconfirmed:
ids = ", ".join([str(eg.id)[:8] for eg in unconfirmed])
contents = "\n".join([f" - {eg.content[:80]}" for eg in unconfimed])
contents = "\n".join([f" - {eg.content[:80]}" for eg in unconfirmed])
return (
f"🧠 Second Brain Heartbeat\n"
f"Unbestätigte Engramme mit gutem Confidence-Score:\n{contents}\n"
@@ -195,8 +206,15 @@ def enrich_context(topic: str, limit: int = 3) -> str:
# memory_context in das Prompt einbauen
"""
store = get_brain()
# Versuche Retriever (mit Embeddings), fallback auf einfache Textsuche
if Retriever:
ret = Retriever(store)
results = ret.retrieve(topic, limit=limit, min_confidence=0.3)
else:
results_raw = store.search_text(topic, limit=limit)
results = [{"engram": eg, "score": 0.5} for eg in results_raw]
if not results:
return ""