feat(memory): v7 — Gedächtnis-Layer mit MCP-Integration

SQLite-basiertes Memory-System fuer persistentes Gedaechtnis ueber Sessions.
Cline, OpenCode und Claude Code teilen denselben Speicher via MCP-Server.

- routers/memory.py: CRUD + Export-Endpoint (GET/POST/PUT/DELETE /api/memory)
- mcp_memory.py: stdio MCP-Server — Tools: get/add/search/update/delete_memory
- MemoryPanel.svelte: Gedaechtnis-Tab mit Filter, Inline-Edit, Add-Formular
- ConnectPanel.svelte: Gedaechtnis-MCP Setup-Guide (Cline/OpenCode/Bosgame)
- Temporaere Eintraege (ephemeral) nach 7 Tagen automatisch geloescht

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Hitonabi
2026-06-23 13:30:47 +02:00
parent 8b52cd20b4
commit e02a1889b2
10 changed files with 617 additions and 28 deletions
+2 -1
View File
@@ -20,7 +20,7 @@ from fastapi import FastAPI, HTTPException
from fastapi.responses import FileResponse, JSONResponse, Response
from fastapi.staticfiles import StaticFiles
from routers import jobs, maintenance, models, system, cookbook, integration, news
from routers import jobs, maintenance, memory, models, system, cookbook, integration, news
app = FastAPI(title="Mission Control")
@@ -43,6 +43,7 @@ app.include_router(system.router)
app.include_router(cookbook.router)
app.include_router(integration.router)
app.include_router(news.router)
app.include_router(memory.router)
_STATIC = Path(__file__).parent / "static"
+1
View File
@@ -20,6 +20,7 @@ MODELS_DIR = Path(os.environ.get("MC_MODELS_DIR", "/srv/models"))
# Eigene Cookbook-Setups (vom Nutzer angelegt). Bewusst NICHT im App-Verzeichnis, sonst
# wuerde ein Deploy (rsync) sie ueberschreiben -> persistent neben den Modellen ablegen.
USER_RECIPES_PATH = Path(os.environ.get("MC_USER_RECIPES", str(MODELS_DIR / "mission-control-recipes.json")))
MEMORY_DB = Path(os.environ.get("MC_MEMORY_DB", str(MODELS_DIR / "mission-control-memory.db")))
# Cache der automatischen Modell-Entdeckung ("aktuell beste Modelle", live von HuggingFace).
# Ebenfalls persistent neben den Modellen (uebersteht Deploys). TTL = wie lange der Cache
# als frisch gilt, bevor lazy neu von den Quellen geladen wird (Default 12 h).
+2
View File
@@ -15,6 +15,7 @@ import CookbookPanel from './panels/CookbookPanel.svelte'
import ConnectPanel from './panels/ConnectPanel.svelte'
import NewsPanel from './panels/NewsPanel.svelte'
import GuidesPanel from './panels/GuidesPanel.svelte'
import MemoryPanel from './panels/MemoryPanel.svelte'
let prevModelStates: Record<string, string> = {}
@@ -156,6 +157,7 @@ const panels: [string, any][] = [
['connect', ConnectPanel],
['news', NewsPanel],
['guides', GuidesPanel],
['memory', MemoryPanel],
]
for (const [view, Panel] of panels) {
const el = document.querySelector(`.view[data-view="${view}"]`)
+65
View File
@@ -11,6 +11,8 @@
let selectedModelIds = $state<string[]>([])
let testResult = $state('')
let testOk = $state(false)
let selectedMemTool = $state('cline')
const memBlock = $derived(memMcpBlock(selectedMemTool))
function baseUrl() { return `http://${location.hostname}:8080/v1` }
function isLanIp(h: string) { return /^\d{1,3}(\.\d{1,3}){3}$/.test(h) }
@@ -71,6 +73,16 @@
function copyVal(inp: HTMLInputElement) {
navigator.clipboard?.writeText(inp.value)
}
function memMcpBlock(tool: string) {
const pyPath = tool === 'bosgame'
? '/opt/mission-control/.venv/bin/python'
: 'python'
const scriptPath = tool === 'bosgame'
? '/opt/mission-control/mcp_memory.py'
: 'C:\\\\Users\\\\TobisPC\\\\mission-control\\\\mcp_memory.py'
return `{\n "mcpServers": {\n "mission-control-memory": {\n "command": "${pyPath}",\n "args": ["${scriptPath}"],\n "env": {\n "MC_URL": "http://192.168.178.151:9000"\n }\n }\n }\n}`
}
</script>
<div class="pagehead">
@@ -308,3 +320,56 @@
</div>
</div>
{/if}
<!-- Memory MCP — unabhaengig von Modellen, immer sichtbar -->
<div class="card">
<div class="card-h">
<h3>Gedächtnis-MCP</h3>
<span class="chip" style="background:rgba(45,212,191,.1);border-color:rgba(45,212,191,.3);color:var(--accent)">Neu in v7</span>
</div>
<div class="card-sub" style="margin-bottom:14px">
Damit Cline, OpenCode und Claude Code sich Projektfakten, Entscheidungen und Präferenzen
über Sessions hinweg merken — geteilt über alle Tools, verwaltet im
<a href="#"
onclick={e => { e.preventDefault(); (document.querySelector(".nav-item[data-view='memory']") as HTMLElement)?.click() }}
style="color:var(--accent)">Gedächtnis-Tab</a>.
</div>
<!-- Tool selector -->
<div class="flex gap-2" style="margin-bottom:14px;flex-wrap:wrap">
{#each [
['cline', 'Cline / Claude Code', 'Windows — Tools auf dem Entwicklungs-PC'],
['opencode', 'OpenCode', 'Windows — Terminal-Agent'],
['bosgame', 'Direkt am Bosgame', 'Wenn das Tool am Server läuft'],
] as [id, label, desc]}
<button class="card-btn{selectedMemTool === id ? ' cb-best' : ''}"
style="text-align:left;padding:10px 14px"
onclick={() => selectedMemTool = id}>
<div style="font-weight:500;font-size:13px">{label}</div>
<div class="card-sub" style="margin:2px 0 0;font-size:11.5px">{desc}</div>
</button>
{/each}
</div>
<!-- Config block -->
<div style="position:relative">
<button class="ghost" style="position:absolute;top:8px;right:8px;z-index:1"
onclick={e => copyCode(e.currentTarget!.parentElement!)}>Kopieren</button>
<div class="log" style="max-height:none"><code>{memBlock}</code></div>
</div>
{#if selectedMemTool === 'cline'}
<p style="margin-top:12px">In <b>Cline</b>: MCP-Einstellungen öffnen (Stecker-Icon in der Cline-Sidebar) → „Add MCP Server" → diesen Block einfügen.</p>
<p>In <b>Claude Code</b>: Block in <code>~/.claude/settings.json</code> unter <code>"mcpServers"</code> einfügen.</p>
<div class="alert warn" style="margin-top:8px;padding:10px 14px"><span class="a-dot"></span>
<span>Einmalig auf dem Windows-PC: <code>pip install mcp httpx</code> ausführen — danach <code>mcp_memory.py</code> aus dem Repo-Ordner erreichbar machen (Pfad im Snippet anpassen).</span>
</div>
{:else if selectedMemTool === 'opencode'}
<p style="margin-top:12px">Block in <code>~/.config/opencode/opencode.json</code> unter <code>"mcpServers"</code> einfügen.</p>
<div class="alert warn" style="margin-top:8px;padding:10px 14px"><span class="a-dot"></span>
<span>Einmalig: <code>pip install mcp httpx</code> — und Pfad zu <code>mcp_memory.py</code> im Snippet anpassen.</span>
</div>
{:else}
<p style="margin-top:12px">Block in die MCP-Config des jeweiligen Tools einfügen. Der Bosgame-Pfad <code>/opt/mission-control/mcp_memory.py</code> ist bereits korrekt — kein weiteres Setup nötig.</p>
{/if}
</div>
+237
View File
@@ -0,0 +1,237 @@
<script lang="ts">
import { api } from '@core/api.js'
import { toast, confirmModal } from '@core/ui.js'
type Mem = {
id: string
content: string
category: string
source: string
created_at: string
updated_at: string
}
let memories = $state<Mem[]>([])
let loading = $state(true)
let filter = $state('all')
let adding = $state(false)
let newContent = $state('')
let newCategory = $state('stable')
let editId = $state<string | null>(null)
let editContent = $state('')
let editCategory = $state('stable')
const filtered = $derived(
filter === 'all' ? memories : memories.filter(m => m.category === filter)
)
const CAT: Record<string, { label: string; color: string }> = {
stable: { label: 'Stabil', color: 'var(--accent)' },
versioned: { label: 'Versioniert', color: '#f59e0b' },
ephemeral: { label: 'Temporär', color: 'var(--mut)' },
}
function relTime(iso: string) {
const s = Math.floor((Date.now() - new Date(iso).getTime()) / 1000)
if (s < 60) return 'gerade eben'
if (s < 3600) return `vor ${Math.floor(s / 60)} Min`
if (s < 86400) return `vor ${Math.floor(s / 3600)} Std`
return `vor ${Math.floor(s / 86400)} Tagen`
}
function countCat(cat: string) {
return memories.filter(m => m.category === cat).length
}
async function load() {
try { memories = await api('/api/memory') }
catch (e: any) { toast('Laden fehlgeschlagen: ' + e.message, true) }
finally { loading = false }
}
async function addMem() {
if (!newContent.trim()) return
try {
const m = await api('/api/memory', {
method: 'POST',
body: JSON.stringify({ content: newContent.trim(), category: newCategory, source: 'manual' })
})
memories = [m, ...memories]
newContent = ''; newCategory = 'stable'; adding = false
toast('Gespeichert')
} catch (e: any) { toast('Fehler: ' + e.message, true) }
}
async function saveMem(m: Mem) {
try {
const updated = await api(`/api/memory/${m.id}`, {
method: 'PUT',
body: JSON.stringify({ content: editContent, category: editCategory })
})
memories = memories.map(x => x.id === m.id ? updated : x)
editId = null
toast('Aktualisiert')
} catch (e: any) { toast('Fehler: ' + e.message, true) }
}
async function delMem(m: Mem) {
const ok = await confirmModal({
title: 'Eintrag löschen?',
body: `„${m.content.length > 80 ? m.content.slice(0, 80) + '…' : m.content}" wird dauerhaft entfernt.`,
danger: true
})
if (!ok) return
try {
await api(`/api/memory/${m.id}`, { method: 'DELETE' })
memories = memories.filter(x => x.id !== m.id)
toast('Gelöscht')
} catch (e: any) { toast('Fehler: ' + e.message, true) }
}
function startEdit(m: Mem) {
editId = m.id; editContent = m.content; editCategory = m.category
}
$effect(() => { load() })
</script>
<div class="pagehead" style="display:flex;justify-content:space-between;align-items:flex-start">
<div>
<h1>Gedächtnis</h1>
<div class="sub">Fakten, Entscheidungen und Kontext — geteilt von allen KI-Tools via MCP.</div>
</div>
<button class="primary" style="flex-shrink:0;margin-top:4px"
onclick={() => { adding = !adding; newContent = '' }}>
{adding ? 'Abbrechen' : '+ Eintrag'}
</button>
</div>
<!-- Add form -->
{#if adding}
<div class="card">
<div class="card-h"><h3>Neuer Eintrag</h3></div>
<label>Inhalt</label>
<textarea
rows="3"
placeholder="z.B.: Mission Control läuft auf Port 9000 — oder: Wir nutzen SQLite statt Qdrant"
style="width:100%;box-sizing:border-box;resize:vertical;margin-bottom:10px"
bind:value={newContent}
onkeydown={e => { if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) addMem() }}
></textarea>
<label>Kategorie</label>
<select bind:value={newCategory} style="margin-bottom:14px">
<option value="stable">Stabil — Projektfakten, die sich selten ändern</option>
<option value="versioned">Versioniert — Tech-Versionen (bei Updates überschreiben)</option>
<option value="ephemeral">Temporär — Aktuelle Aufgabe (nach 7 Tagen gelöscht)</option>
</select>
<div class="btn-row">
<button class="primary" onclick={addMem} disabled={!newContent.trim()}>Speichern</button>
<button class="ghost" onclick={() => { adding = false; newContent = '' }}>Abbrechen</button>
<span class="card-sub" style="font-size:11.5px;margin-left:4px">Strg+Enter zum Speichern</span>
</div>
</div>
{/if}
<!-- Category filter -->
<div class="card" style="padding:10px 16px">
<div class="flex gap-2" style="flex-wrap:wrap;align-items:center">
<span style="font-size:11.5px;color:var(--mut);margin-right:4px">Filter:</span>
{#each [
['all', 'Alle', '', memories.length],
['stable', 'Stabil', 'var(--accent)', countCat('stable')],
['versioned', 'Versioniert', '#f59e0b', countCat('versioned')],
['ephemeral', 'Temporär', 'var(--mut)', countCat('ephemeral')],
] as [val, lbl, clr, cnt]}
<span class="chip"
role="button" tabindex="0"
style="cursor:pointer;{filter === val
? `background:rgba(45,212,191,.12);border-color:${clr || 'var(--accent)'};color:${clr || 'var(--accent)'}`
: ''}"
onclick={() => filter = val}
onkeydown={e => e.key === 'Enter' && (filter = val)}>
{lbl} <span style="color:var(--mut);margin-left:2px">{cnt}</span>
</span>
{/each}
</div>
</div>
<!-- Memory list -->
{#if loading}
<div class="card"><div class="card-sub">Lade…</div></div>
{:else if filtered.length === 0}
<div class="card">
<div class="empty-c" style="padding:8px 0 4px">
<div class="e-t">{memories.length === 0 ? 'Noch keine Einträge' : 'Keine Treffer'}</div>
<div class="e-s">
{memories.length === 0
? 'Klicke „+ Eintrag" um manuell etwas zu speichern, oder verbinde ein KI-Tool via MCP.'
: 'Andere Kategorie wählen oder Eintrag anlegen.'}
</div>
{#if memories.length === 0}
<button class="primary" style="margin-top:14px" onclick={() => adding = true}>
+ Ersten Eintrag anlegen
</button>
{/if}
</div>
</div>
{:else}
<div class="card" style="padding:0;overflow:hidden">
{#each filtered as m, i (m.id)}
<div style="padding:14px 16px;{i < filtered.length - 1 ? 'border-bottom:1px solid var(--brd, rgba(255,255,255,.07))' : ''}">
{#if editId === m.id}
<textarea
rows="3"
style="width:100%;box-sizing:border-box;resize:vertical;margin-bottom:8px"
bind:value={editContent}
></textarea>
<select bind:value={editCategory} style="margin-bottom:10px">
<option value="stable">Stabil</option>
<option value="versioned">Versioniert</option>
<option value="ephemeral">Temporär</option>
</select>
<div class="btn-row">
<button class="primary" onclick={() => saveMem(m)}>Speichern</button>
<button class="ghost" onclick={() => editId = null}>Abbrechen</button>
</div>
{:else}
<div style="display:flex;gap:10px;align-items:flex-start">
<div style="flex:1;min-width:0">
<div style="font-size:13.5px;line-height:1.55;word-break:break-word">{m.content}</div>
<div class="flex gap-2" style="margin-top:8px;flex-wrap:wrap;align-items:center">
<span class="chip" style="border-color:{CAT[m.category]?.color || 'var(--mut)'};color:{CAT[m.category]?.color || 'var(--mut)'}">
{CAT[m.category]?.label || m.category}
</span>
<span class="chip">{m.source}</span>
<span style="font-size:11px;color:var(--mut)">{relTime(m.updated_at)}</span>
</div>
</div>
<div class="flex gap-2" style="flex-shrink:0;margin-top:2px">
<button class="ghost" style="padding:4px 10px;font-size:12px"
onclick={() => startEdit(m)}>Bearbeiten</button>
<button class="ghost" style="padding:4px 10px;font-size:12px;color:var(--err)"
onclick={() => delMem(m)}>Löschen</button>
</div>
</div>
{/if}
</div>
{/each}
</div>
{/if}
<!-- Info-Box: wie KI-Tools das Gedächtnis nutzen -->
<div class="card" style="background:rgba(45,212,191,.04);border-color:rgba(45,212,191,.2)">
<div class="card-h"><h3 style="font-size:13px;font-weight:500">Wie KI-Tools dieses Gedächtnis nutzen</h3></div>
<div class="card-sub" style="line-height:1.6">
Cline, OpenCode und Claude Code greifen über den <b>Memory MCP-Server</b> auf diese Einträge zu.
Beim Start einer Session lädt der Agent automatisch alle Einträge — er kennt dann deinen Stack,
deine Präferenzen und frühere Entscheidungen. Den Setup-Guide findest du unter
<a href="#"
onclick={e => { e.preventDefault(); (document.querySelector(".nav-item[data-view='connect']") as HTMLElement)?.click() }}
style="color:var(--accent)">Verbinden → Gedächtnis-MCP</a>.
</div>
<div class="card-sub" style="margin-top:8px;font-size:11.5px">
Kategorien: <b style="color:var(--accent)">Stabil</b> = Projektfakten die sich selten ändern ·
<b style="color:#f59e0b">Versioniert</b> = Tech-Versionen (bei Updates überschreiben) ·
<b style="color:var(--mut)">Temporär</b> = aktuelle Aufgaben, nach 7 Tagen automatisch gelöscht
</div>
</div>
+113
View File
@@ -0,0 +1,113 @@
#!/usr/bin/env python3
"""
Mission Control — Memory MCP Server
Stdio-MCP-Server fuer persistentes Gedaechtnis.
Laeuft als Subprocess von Cline / Claude Code / OpenCode auf dem Client-Rechner.
Ruft die /api/memory-Endpunkte von Mission Control via HTTP auf.
Konfiguration via Env-Vars:
MC_URL Mission Control URL (default: http://192.168.178.151:9000)
MC_TOKEN Auth-Token (default: leer)
Einmalig installieren (auf dem Rechner wo das KI-Tool laeuft):
pip install mcp httpx
"""
import os
import httpx
from mcp.server.fastmcp import FastMCP
MC_URL = os.environ.get("MC_URL", "http://192.168.178.151:9000").rstrip("/")
MC_TOKEN = os.environ.get("MC_TOKEN", "")
mcp = FastMCP("mission-control-memory")
def _headers() -> dict:
return {"X-MC-Token": MC_TOKEN} if MC_TOKEN else {}
def _get(path: str, **params) -> list | dict:
r = httpx.get(f"{MC_URL}{path}", headers=_headers(), params=params, timeout=10)
r.raise_for_status()
return r.json()
def _post(path: str, data: dict) -> dict:
r = httpx.post(f"{MC_URL}{path}", headers=_headers(), json=data, timeout=10)
r.raise_for_status()
return r.json()
def _put(path: str, data: dict) -> dict:
r = httpx.put(f"{MC_URL}{path}", headers=_headers(), json=data, timeout=10)
r.raise_for_status()
return r.json()
def _delete(path: str) -> dict:
r = httpx.delete(f"{MC_URL}{path}", headers=_headers(), timeout=10)
r.raise_for_status()
return r.json()
@mcp.tool()
def get_memories(category: str = "") -> str:
"""Laedt gespeicherte Fakten und Entscheidungen. category: stable | versioned | ephemeral | (leer = alle)"""
params = {"category": category} if category else {}
items = _get("/api/memory", **params)
if not items:
return "Keine Memories gespeichert."
cat_icon = {"stable": "🔵", "versioned": "🟡", "ephemeral": ""}
return "\n".join(
f"{cat_icon.get(m['category'], '·')} [{m['category']}] {m['content']}"
f" (Quelle: {m['source']}, ID: {m['id'][:8]})"
for m in items
)
@mcp.tool()
def add_memory(content: str, category: str = "stable", source: str = "agent") -> str:
"""Speichert einen Fakt oder eine Entscheidung.
category: stable=Projektfakten (selten aendern), versioned=Tech-Versionen (bei Updates ueberschreiben), ephemeral=temporaerer Kontext (nach 7 Tagen geloescht).
"""
m = _post("/api/memory", {"content": content, "category": category, "source": source})
return f"Gespeichert (ID: {m['id'][:8]}): {content}"
@mcp.tool()
def search_memories(q: str) -> str:
"""Sucht per Stichwort in allen gespeicherten Fakten."""
items = _get("/api/memory", q=q)
if not items:
return f"Keine Treffer fuer '{q}'."
return "\n".join(
f"[{m['category']}] {m['content']} (ID: {m['id'][:8]})"
for m in items
)
@mcp.tool()
def update_memory(memory_id: str, content: str = "", category: str = "") -> str:
"""Aktualisiert einen bestehenden Eintrag (z.B. nach Tech-Update). Nur gesetzte Felder werden geaendert."""
data: dict = {}
if content:
data["content"] = content
if category:
data["category"] = category
if not data:
return "Nichts zu aktualisieren (content und category sind leer)."
m = _put(f"/api/memory/{memory_id}", data)
return f"Aktualisiert: {m['content']}"
@mcp.tool()
def delete_memory(memory_id: str) -> str:
"""Loescht einen veralteten oder falschen Eintrag anhand seiner ID."""
_delete(f"/api/memory/{memory_id}")
return f"Eintrag {memory_id[:8]} geloescht."
if __name__ == "__main__":
mcp.run()
+1
View File
@@ -4,3 +4,4 @@ httpx>=0.27
ruamel.yaml>=0.18
psutil>=5.9.0
huggingface_hub>=0.34 # liefert die `hf`-CLI fuer Modell-Downloads
mcp>=1.0 # MCP Python SDK fuer mcp_memory.py stdio-Server
+154
View File
@@ -0,0 +1,154 @@
"""
Memory Layer fuer Mission Control.
Persistentes Gedaechtnis fuer lokale LLM-Agenten (Cline, Claude Code, OpenCode).
SQLite aus stdlib — kein Vektor-Overhead, keine neue Abhaengigkeit.
Alle MCP-Tools teilen denselben Speicher via mcp_memory.py-Wrapper.
"""
import sqlite3
import uuid
from datetime import datetime, timezone
from typing import Optional
from fastapi import APIRouter, Depends, HTTPException
from pydantic import BaseModel
from auth import auth
from config import MEMORY_DB
router = APIRouter(prefix="/api", dependencies=[Depends(auth)])
_db_conn: Optional[sqlite3.Connection] = None
def _db() -> sqlite3.Connection:
global _db_conn
if _db_conn is None:
MEMORY_DB.parent.mkdir(parents=True, exist_ok=True)
_db_conn = sqlite3.connect(str(MEMORY_DB), check_same_thread=False)
_db_conn.row_factory = sqlite3.Row
_db_conn.execute("""
CREATE TABLE IF NOT EXISTS memories (
id TEXT PRIMARY KEY,
content TEXT NOT NULL,
category TEXT NOT NULL DEFAULT 'stable',
source TEXT NOT NULL DEFAULT 'manual',
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL
)
""")
# Temporaere Eintraege nach 7 Tagen automatisch loeschen
_db_conn.execute(
"DELETE FROM memories WHERE category = 'ephemeral'"
" AND datetime(created_at) < datetime('now', '-7 days')"
)
_db_conn.commit()
return _db_conn
class _MemIn(BaseModel):
content: str
category: str = "stable" # stable | versioned | ephemeral
source: str = "manual"
class _MemUp(BaseModel):
content: Optional[str] = None
category: Optional[str] = None
def _row(r: sqlite3.Row) -> dict:
return dict(r)
# Export muss vor /{mid} stehen, sonst wuerde FastAPI "export" als ID behandeln
@router.get("/memory/export")
def export_memories():
"""Alle Eintraege als strukturierter Plain-Text — direkt als System-Prompt-Kontext nutzbar."""
db = _db()
rows = db.execute(
"SELECT * FROM memories ORDER BY category, updated_at DESC"
).fetchall()
cat_names = {
"stable": "Stabile Fakten",
"versioned": "Versionierte Infos",
"ephemeral": "Temporaerer Kontext",
}
lines = ["# Mission Control — Gedaechtnis\n"]
current = ""
for r in rows:
cat = cat_names.get(r["category"], r["category"])
if cat != current:
lines.append(f"\n## {cat}\n")
current = cat
lines.append(
f"- {r['content']} _(Quelle: {r['source']}, {r['updated_at'][:10]})_"
)
return {"text": "\n".join(lines), "count": len(rows)}
@router.get("/memory")
def list_memories(q: str = "", category: str = ""):
db = _db()
sql = "SELECT * FROM memories"
params: list = []
conds: list = []
if q:
conds.append("content LIKE ?")
params.append(f"%{q}%")
if category:
conds.append("category = ?")
params.append(category)
if conds:
sql += " WHERE " + " AND ".join(conds)
sql += " ORDER BY created_at DESC"
return [_row(r) for r in db.execute(sql, params).fetchall()]
@router.post("/memory", status_code=201)
def add_memory(body: _MemIn):
now = datetime.now(timezone.utc).isoformat()
mid = str(uuid.uuid4())
db = _db()
db.execute(
"INSERT INTO memories (id, content, category, source, created_at, updated_at)"
" VALUES (?,?,?,?,?,?)",
(mid, body.content.strip(), body.category, body.source, now, now),
)
db.commit()
return {
"id": mid, "content": body.content.strip(),
"category": body.category, "source": body.source,
"created_at": now, "updated_at": now,
}
@router.put("/memory/{mid}")
def update_memory(mid: str, body: _MemUp):
db = _db()
row = db.execute("SELECT * FROM memories WHERE id = ?", (mid,)).fetchone()
if not row:
raise HTTPException(404, "Eintrag nicht gefunden")
now = datetime.now(timezone.utc).isoformat()
content = body.content.strip() if body.content is not None else row["content"]
category = body.category if body.category is not None else row["category"]
db.execute(
"UPDATE memories SET content=?, category=?, updated_at=? WHERE id=?",
(content, category, now, mid),
)
db.commit()
return {
"id": mid, "content": content, "category": category,
"source": row["source"], "created_at": row["created_at"], "updated_at": now,
}
@router.delete("/memory/{mid}")
def delete_memory(mid: str):
db = _db()
if not db.execute("SELECT id FROM memories WHERE id = ?", (mid,)).fetchone():
raise HTTPException(404, "Eintrag nicht gefunden")
db.execute("DELETE FROM memories WHERE id = ?", (mid,))
db.commit()
return {"ok": True}
+40 -27
View File
File diff suppressed because one or more lines are too long
+2
View File
@@ -22,6 +22,7 @@
<a class="nav-item" data-view="connect"><span class="ni-ic" data-ic="swap"></span><span class="ni-tx">Verbinden</span></a>
<a class="nav-item" data-view="news"><span class="ni-ic" data-ic="info"></span><span class="ni-tx">News</span></a>
<a class="nav-item" data-view="guides"><span class="ni-ic" data-ic="help"></span><span class="ni-tx">Guides</span></a>
<a class="nav-item" data-view="memory"><span class="ni-ic" data-ic="database"></span><span class="ni-tx">Gedächtnis</span></a>
</nav>
<div class="side-foot">
<span class="nav-item" id="nav-settings"><span class="ni-ic" data-ic="settings"></span><span class="ni-tx">Einstellungen</span></span>
@@ -48,6 +49,7 @@
<section class="view" data-view="connect" hidden></section>
<section class="view" data-view="news" hidden></section>
<section class="view" data-view="guides" hidden></section>
<section class="view" data-view="memory" hidden></section>
</main>
</div>
</div>