diff --git a/aria-brain/main.py b/aria-brain/main.py
index 5b4708a..bcfa680 100644
--- a/aria-brain/main.py
+++ b/aria-brain/main.py
@@ -398,6 +398,19 @@ def memory_delete(point_id: str):
# ─── Memory-Anhaenge ──────────────────────────────────────────────────
+@app.get("/attachments/limit")
+def memory_attachments_limit():
+ """Aktuelles Groessen-Limit fuer Memory-Anhaenge.
+
+ Die Oberflaechen zeigen damit die ECHTE Zahl an statt eines fest
+ eingetippten "max 20 MB" — das stand vorher im HTML und stimmte nicht mehr,
+ sobald jemand das Limit anhob.
+ """
+ import memory_attachments as mem_att
+ limit = mem_att.max_bytes()
+ return {"maxBytes": limit, "maxMb": round(limit / 1024 / 1024, 1)}
+
+
@app.get("/memory/{point_id}/attachments")
def memory_attachments_list(point_id: str):
"""Liste der Anhaenge zum Memory. Source-of-Truth ist das Payload
diff --git a/aria-brain/memory_attachments.py b/aria-brain/memory_attachments.py
index 071e382..daffcaa 100644
--- a/aria-brain/memory_attachments.py
+++ b/aria-brain/memory_attachments.py
@@ -16,6 +16,7 @@ selbst hat in Stufe A kein Tool zum Upload.
from __future__ import annotations
import base64
+import json
import logging
import mimetypes
import os
@@ -27,9 +28,41 @@ from typing import List, Optional
logger = logging.getLogger(__name__)
ROOT = Path(os.environ.get("MEMORY_ATTACHMENTS_DIR", "/shared/memory-attachments"))
-MAX_BYTES = int(os.environ.get("MEMORY_ATTACHMENT_MAX_BYTES", str(20 * 1024 * 1024))) # 20 MB
SAFE_NAME_RE = re.compile(r"[^A-Za-z0-9._\-]")
+RUNTIME_CONFIG_FILE = Path("/shared/config/runtime.json")
+# 64 MB: so viel holt die App auch auf ausdruecklichen Tipp noch herunter
+# (TAP_DL_MAX_BYTES in ChatScreen.tsx). Ein hoeheres Limit wuerde Anhaenge
+# erlauben, die das Handy anschliessend nicht mehr laden kann.
+_DEFAULT_MAX_BYTES = 64 * 1024 * 1024
+
+
+def max_bytes() -> int:
+ """Aktuelles Groessen-Limit fuer Anhaenge.
+
+ Reihenfolge: runtime.json (Diagnostic → System & Wartung) > ENV > Default.
+ Wird bei JEDEM Aufruf gelesen, damit eine Aenderung in der Diagnostic sofort
+ greift — ein Brain-Neustart nur fuers Anheben einer Zahl waere Unsinn.
+ """
+ try:
+ with RUNTIME_CONFIG_FILE.open() as f:
+ mb = json.load(f).get("memoryAttachmentMaxMb")
+ if mb is not None:
+ val = int(float(mb) * 1024 * 1024)
+ if val > 0:
+ return val
+ except FileNotFoundError:
+ pass
+ except Exception as exc:
+ logger.warning("runtime.json lesen fehlgeschlagen: %s", exc)
+ env = os.environ.get("MEMORY_ATTACHMENT_MAX_BYTES")
+ if env:
+ try:
+ return int(env)
+ except ValueError:
+ logger.warning("MEMORY_ATTACHMENT_MAX_BYTES ist keine Zahl: %r", env)
+ return _DEFAULT_MAX_BYTES
+
def _safe_filename(name: str) -> str:
"""Macht aus einem User-Namen einen filesystem-sicheren String —
@@ -78,8 +111,12 @@ def save_attachment(memory_id: str, filename: str, data: bytes) -> dict:
Ueberschreibt eine bestehende Datei mit gleichem Namen."""
if not memory_id:
raise ValueError("memory_id ist Pflicht")
- if len(data) > MAX_BYTES:
- raise ValueError(f"Anhang zu gross ({len(data)} > {MAX_BYTES} Byte)")
+ limit = max_bytes()
+ if len(data) > limit:
+ raise ValueError(
+ f"Anhang zu gross ({len(data) // (1024 * 1024)} MB > "
+ f"{limit // (1024 * 1024)} MB). Das Limit steht in der Diagnostic "
+ f"unter System & Wartung.")
safe = _safe_filename(filename)
d = memory_dir(memory_id)
d.mkdir(parents=True, exist_ok=True)
@@ -165,8 +202,12 @@ def attach_from_path(memory_id: str, source_path: str) -> dict:
if not src.is_file():
raise ValueError(f"Datei nicht gefunden: {source_path}")
size = src.stat().st_size
- if size > MAX_BYTES:
- raise ValueError(f"Datei zu gross ({size} > {MAX_BYTES} Byte)")
+ limit = max_bytes()
+ if size > limit:
+ raise ValueError(
+ f"Datei zu gross ({size // (1024 * 1024)} MB > "
+ f"{limit // (1024 * 1024)} MB). Das Limit steht in der Diagnostic "
+ f"unter System & Wartung.")
# Reuse save_attachment damit Filename-Sanitization + Logging konsistent
data = src.read_bytes()
return save_attachment(memory_id, src.name, data)
diff --git a/diagnostic/index.html b/diagnostic/index.html
index a31cdeb..a39d25e 100644
--- a/diagnostic/index.html
+++ b/diagnostic/index.html
@@ -1051,6 +1051,31 @@
+
+
+
Grösse von Anhängen
+
+
+ Grösste Datei, die an einen Gedanken angehängt werden darf.
+ Greift sofort — kein Neustart nötig.
+
+
+
+ MB
+
+
+
+ Über 64 MB lädt die Handy-App Anhänge nicht mehr herunter —
+ ein Anhang muss als Base64 komplett in den Speicher, und daran ist die App
+ schon einmal gestorben. Er liegt dann sauber auf dem Server, ist mobil aber
+ nur noch über den Dateimanager erreichbar.
+
+
+
+
+
Reparatur & Restart
@@ -1371,9 +1396,42 @@
+
+
+
+
+
+ von
+
+ bis
+
+
+
+
+
+
+
+
+ Nur ein Feld ausfüllen = offenes Ende
+
+
Mehrere Begriffe mit AND/OR verknuepfen — Volltext-Substring, case-insensitive, links-nach-rechts ausgewertet.
@@ -1732,7 +1790,7 @@
@@ -2650,6 +2708,26 @@
return;
}
+ if (msg.type === 'attachment_limit_info') {
+ const inp = document.getElementById('attachment-limit-input');
+ const st = document.getElementById('attachment-limit-status');
+ if (inp && msg.maxMb != null) inp.value = msg.maxMb;
+ // Das Label an der Upload-Stelle im Gehirn-Tab mitziehen, damit dort
+ // nicht weiter ein festes "max 20 MB" steht das nicht mehr stimmt.
+ if (msg.maxMb != null) {
+ attachmentLimitMb = msg.maxMb;
+ document.querySelectorAll('.attachment-limit-label').forEach(el => {
+ el.textContent = 'max ' + msg.maxMb + ' MB pro Datei';
+ });
+ }
+ if (st) {
+ st.textContent = msg.error ? ('Fehler: ' + msg.error) : (msg.info || '');
+ st.style.color = msg.error ? '#FF6B6B'
+ : (msg.maxMb > (msg.softMb || 64) ? '#FFD60A' : '#3FFF9F');
+ }
+ return;
+ }
+
// session_restarted / openclaw_config WS-Events entfernt — aria-core ist raus.
if (msg.type === 'model_info') {
const el = document.getElementById('setting-model');
@@ -4543,6 +4621,22 @@
// ── Haupt-Tab Navigation ──────────────────────────────────
+ // Zuletzt bekanntes Anhang-Limit (MB) — nur fuer Anzeige/Vorab-Pruefung.
+ // Die verbindliche Pruefung macht das Brain.
+ let attachmentLimitMb = null;
+
+ function saveAttachmentLimit() {
+ const inp = document.getElementById('attachment-limit-input');
+ const st = document.getElementById('attachment-limit-status');
+ const v = Number(inp && inp.value);
+ if (!isFinite(v) || v <= 0) {
+ if (st) { st.textContent = 'Bitte eine Zahl grösser 0 angeben.'; st.style.color = '#FF6B6B'; }
+ return;
+ }
+ if (st) { st.textContent = 'Speichere …'; st.style.color = '#8888AA'; }
+ send({ action: 'set_attachment_limit', maxMb: v });
+ }
+
function switchSettingsTab(sub) {
sub = sub || 'models';
document.querySelectorAll('.settings-subtab').forEach(el => el.classList.toggle('visible', el.dataset.sub === sub));
@@ -4567,9 +4661,12 @@
loadOAuthServices();
loadJellyfinConfig();
refreshVoiceIdStatus();
+ send({ action: 'get_attachment_limit' });
} else if (tab === 'brain') {
loadBrainStatus();
loadBrainMemoryList();
+ // Damit das Anhang-Label die echte Zahl zeigt, nicht eine fest eingetippte.
+ send({ action: 'get_attachment_limit' });
refreshImportFiles();
loadMetrics();
loadProjects();
@@ -6493,6 +6590,14 @@
const t = document.getElementById('brain-filter-type'); if (t) t.value = '';
const p = document.getElementById('brain-filter-pinned'); if (p) p.value = 'all';
const info = document.getElementById('brain-search-info'); if (info) info.style.display = 'none';
+ const so = document.getElementById('brain-sort'); if (so) so.value = 'type';
+ // Zeitraum-Panel zu + leeren, sonst filtert es nach dem ✕ unsichtbar weiter
+ const dp = document.getElementById('brain-date-panel');
+ if (dp) dp.style.display = 'none';
+ const db = document.getElementById('btn-date-filter');
+ if (db) db.style.color = '#8888AA';
+ const df = document.getElementById('brain-date-from'); if (df) df.value = '';
+ const dt = document.getElementById('brain-date-to'); if (dt) dt.value = '';
brainSearchIds = null;
clearAdvancedSearch();
}
@@ -6745,7 +6850,10 @@
if (!el) return;
// Wenn aktive Search-Treffer da sind: die anzeigen, nicht neu laden
if (brainSearchIds && brainSearchIds.length) {
- const items = brainSearchIds.map(id => brainMemoryCache[id]).filter(Boolean);
+ // Zeitraum gilt auch fuer Such-Treffer — sonst waere "suche X,
+ // aber nur von letzter Woche" nicht moeglich.
+ const items = applyDateFilter(
+ brainSearchIds.map(id => brainMemoryCache[id]).filter(Boolean));
renderBrainList(items, true);
return;
}
@@ -6761,8 +6869,12 @@
items.forEach(m => { brainMemoryCache[m.id] = m; });
if (pinnedFilter === 'pinned') items = items.filter(m => m.pinned);
else if (pinnedFilter === 'cold') items = items.filter(m => !m.pinned);
+ const before = items.length;
+ items = applyDateFilter(items);
if (!items.length) {
- el.innerHTML = '(Keine Memories — leere DB oder Filter zu eng)';
+ el.innerHTML = before > 0
+ ? `(Kein Gedanke im gewählten Zeitraum — ${before} wurden durch den Zeitfilter ausgeblendet)`
+ : '(Keine Memories — leere DB oder Filter zu eng)';
return;
}
renderBrainList(items, false);
@@ -6831,6 +6943,31 @@
`);
}
+ // ─── Datum/Uhrzeit eines Gedankens ────────────────────────────────
+ // created_at / updated_at liegen schon immer als ISO-String in der DB —
+ // sie wurden nur nie angezeigt. Darum sah man nicht, was der letzte
+ // Eintrag war.
+ function memTs(m, which) {
+ const raw = which === 'created' ? m.created_at : (m.updated_at || m.created_at);
+ if (!raw) return 0;
+ const t = Date.parse(raw);
+ return isFinite(t) ? t : 0;
+ }
+ function fmtMemTs(ms) {
+ if (!ms) return '';
+ return new Date(ms).toLocaleString('de-DE',
+ { day: '2-digit', month: '2-digit', year: '2-digit', hour: '2-digit', minute: '2-digit' });
+ }
+ /** "vor 3 Min" / "vor 2 Std" / "vor 5 Tagen" — fuer "was war der letzte Eintrag". */
+ function fmtMemAgo(ms) {
+ if (!ms) return '';
+ const s = Math.max(0, Math.round((Date.now() - ms) / 1000));
+ if (s < 60) return 'gerade';
+ 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)} Tg`;
+ }
+
function renderMemoryRow(m, withScore) {
const pin = m.pinned ? '📌 ' : '';
const preview = (m.content || '').slice(0, 140).replace(/\n/g, ' ');
@@ -6844,12 +6981,25 @@
? `SYS`
: `PRIV`)
: '';
+ // Zeitstempel-Zeile: angelegt immer, geaendert nur wenn es sich
+ // unterscheidet (sonst stehen zwei identische Zeiten nebeneinander).
+ const cMs = memTs(m, 'created');
+ const uMs = memTs(m, 'updated');
+ const tsLine = (cMs || uMs)
+ ? `