feat(diagnostic): Anhang-Limit einstellbar + Datum/Uhrzeit bei Gedanken
Zwei Dinge, die bisher nur im Code bzw. nur in der DB standen. Anhang-Limit (Einstellungen → System & Wartung): - Das Limit war eine Env-Variable mit 20 MB Default — zum Anheben musste man die .env editieren und das Brain neu starten. Jetzt steht es in runtime.json und das Brain liest es bei JEDEM Upload neu: Aenderung greift sofort. - Reihenfolge: runtime.json > ENV > Default. Anders als beim OpenAI-Endpunkt gewinnt hier NICHT die .env — dort erzwingt ein Docker-Port-Mapping das, hier gibt es keinen solchen Zwang, und eine Einstellung die nichts tut waere schlimmer als keine. - Default von 20 auf 64 MB. Genau so viel holt die App auch auf ausdruecklichen Tipp noch herunter (TAP_DL_MAX_BYTES); darueber warnt die UI, verbietet es aber nicht — der Anhang liegt dann sauber auf dem Server, ist mobil nur nicht mehr ladbar. - Neuer Endpunkt GET /attachments/limit. Das fest eingetippte "max 20 MB pro Datei" im HTML ist weg — es stimmte nach jeder Aenderung nicht mehr. - Die Fehlermeldung nennt jetzt MB statt Bytes und sagt, wo die Einstellung ist. Datum/Uhrzeit bei Gedanken: - created_at/updated_at lagen schon immer in der DB und wurden ausgeliefert — nur nie angezeigt. Jede Zeile zeigt jetzt "angelegt <Zeit>" und, falls abweichend, "geaendert <Zeit>" plus ein relatives "(vor 3 Std)". Damit sieht man auf den ersten Blick, was der letzte Eintrag war. - Sortier-Dropdown: nach Typ gruppiert (wie bisher) oder nach Anlage/Aenderung auf- und absteigend. Bei Zeit-Sortierung wird die Liste flach dargestellt — man will sehen was zuletzt passiert ist, nicht in welche Schublade es gehoert. - Zeitraum-Filter (🕒) mit von/bis, umschaltbar zwischen angelegt und geaendert, plus Schnellwahl Heute / 7 / 30 Tage. Gilt auch fuer Such-Treffer, damit "suche X, aber nur letzte Woche" geht. Zuklappen schaltet ihn aus, und das ✕-Zuruecksetzen raeumt ihn mit auf (sonst filtert er unsichtbar weiter). Beides gegen den echten Code getestet: 7 Faelle fuers Limit (inkl. kaputter runtime.json und einem 50-MB-PDF an beiden Grenzen), 10 fuer Sortierung und Zeitraum. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
+231
-5
@@ -1051,6 +1051,31 @@
|
||||
|
||||
<div class="settings-subtab" data-sub="system">
|
||||
|
||||
<!-- Anhang-Limit -->
|
||||
<div class="settings-section">
|
||||
<h2>Grösse von Anhängen</h2>
|
||||
<div class="card" style="max-width:560px;line-height:1.6;">
|
||||
<p style="color:#8888AA;font-size:12px;margin:0 0 10px;">
|
||||
Grösste Datei, die an einen Gedanken angehängt werden darf.
|
||||
Greift <strong>sofort</strong> — kein Neustart nötig.
|
||||
</p>
|
||||
<div style="display:flex;gap:8px;align-items:center;flex-wrap:wrap;">
|
||||
<input type="number" id="attachment-limit-input" min="1" max="1024" step="1"
|
||||
style="width:110px;background:#12121E;border:1px solid #2A2A3E;border-radius:6px;
|
||||
color:#E0E0F0;padding:8px 10px;font-size:13px;">
|
||||
<span style="color:#8888AA;font-size:12px;">MB</span>
|
||||
<button class="btn secondary" onclick="saveAttachmentLimit()">Speichern</button>
|
||||
</div>
|
||||
<p style="color:#8888AA;font-size:11px;margin:10px 0 0;">
|
||||
Über <strong>64 MB</strong> 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.
|
||||
</p>
|
||||
<div id="attachment-limit-status" style="margin-top:10px;font-size:11px;color:#8888AA;"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Reparatur & Restart -->
|
||||
<div class="settings-section">
|
||||
<h2>Reparatur & Restart</h2>
|
||||
@@ -1371,9 +1396,42 @@
|
||||
<option value="pinned">📌 Nur Pinned</option>
|
||||
<option value="cold">Nur Cold</option>
|
||||
</select>
|
||||
<select id="brain-sort" onchange="onBrainFiltersChanged()"
|
||||
style="background:#12121E;border:1px solid #2A2A3E;border-radius:6px;color:#E0E0F0;padding:4px 8px;font-size:11px;"
|
||||
title="Sortierung der Liste">
|
||||
<option value="type">Nach Typ gruppiert</option>
|
||||
<option value="updated_desc">Neueste Änderung zuerst</option>
|
||||
<option value="updated_asc">Älteste Änderung zuerst</option>
|
||||
<option value="created_desc">Neueste Anlage zuerst</option>
|
||||
<option value="created_asc">Älteste Anlage zuerst</option>
|
||||
</select>
|
||||
<button class="btn secondary" onclick="toggleAdvancedSearch()" id="btn-advanced-search" style="padding:4px 8px;font-size:11px;color:#8888AA;" title="Erweiterte Suche mit AND/OR-Verknuepfungen">⌃ Erweitert</button>
|
||||
<button class="btn secondary" onclick="toggleDateFilter()" id="btn-date-filter" style="padding:4px 8px;font-size:11px;color:#8888AA;" title="Nach Zeitraum filtern">🕒 Zeitraum</button>
|
||||
<button class="btn secondary" onclick="resetBrainFilters();loadBrainMemoryList()" style="padding:4px 8px;font-size:11px;color:#8888AA;" title="Suche + Filter zurücksetzen">✕</button>
|
||||
</div>
|
||||
<div id="brain-date-panel" style="display:none;margin-top:10px;padding:10px;background:#080810;border:1px solid #1E1E2E;border-radius:6px;">
|
||||
<div style="display:flex;gap:8px;align-items:center;flex-wrap:wrap;font-size:11px;color:#8888AA;">
|
||||
<select id="brain-date-field"
|
||||
style="background:#12121E;border:1px solid #2A2A3E;border-radius:6px;color:#E0E0F0;padding:4px 8px;font-size:11px;">
|
||||
<option value="updated">geändert</option>
|
||||
<option value="created">angelegt</option>
|
||||
</select>
|
||||
<span>von</span>
|
||||
<input type="datetime-local" id="brain-date-from"
|
||||
style="background:#12121E;border:1px solid #2A2A3E;border-radius:6px;color:#E0E0F0;padding:4px 8px;font-size:11px;">
|
||||
<span>bis</span>
|
||||
<input type="datetime-local" id="brain-date-to"
|
||||
style="background:#12121E;border:1px solid #2A2A3E;border-radius:6px;color:#E0E0F0;padding:4px 8px;font-size:11px;">
|
||||
<button class="btn" onclick="onBrainFiltersChanged()" style="padding:4px 12px;font-size:11px;">Filtern</button>
|
||||
<button class="btn secondary" onclick="clearDateFilter()" style="padding:4px 10px;font-size:11px;color:#8888AA;">Leeren</button>
|
||||
</div>
|
||||
<div style="display:flex;gap:6px;margin-top:8px;flex-wrap:wrap;">
|
||||
<button class="btn secondary" onclick="setDateQuick(1)" style="padding:3px 9px;font-size:10px;">Heute</button>
|
||||
<button class="btn secondary" onclick="setDateQuick(7)" style="padding:3px 9px;font-size:10px;">7 Tage</button>
|
||||
<button class="btn secondary" onclick="setDateQuick(30)" style="padding:3px 9px;font-size:10px;">30 Tage</button>
|
||||
<span style="color:#555570;font-size:10px;margin-left:auto;">Nur ein Feld ausfüllen = offenes Ende</span>
|
||||
</div>
|
||||
</div>
|
||||
<div id="brain-advanced-panel" style="display:none;margin-top:10px;padding:10px;background:#080810;border:1px solid #1E1E2E;border-radius:6px;">
|
||||
<div style="color:#8888AA;font-size:11px;margin-bottom:6px;">
|
||||
Mehrere Begriffe mit AND/OR verknuepfen — Volltext-Substring, case-insensitive, links-nach-rechts ausgewertet.
|
||||
@@ -1732,7 +1790,7 @@
|
||||
<div id="memory-attachments-block" style="display:none;margin-top:14px;padding-top:10px;border-top:1px solid #1E1E2E;">
|
||||
<label style="display:flex;align-items:center;justify-content:space-between;font-size:11px;color:#8888AA;margin-bottom:6px;">
|
||||
<span>📎 Anhaenge</span>
|
||||
<span style="color:#555570;font-size:10px;">max 20 MB pro Datei</span>
|
||||
<span class="attachment-limit-label" style="color:#555570;font-size:10px;">Limit: Einstellungen → System & Wartung</span>
|
||||
</label>
|
||||
<div id="memory-attachments-list" style="display:flex;flex-direction:column;gap:4px;margin-bottom:6px;font-size:12px;color:#555570;"></div>
|
||||
<div style="display:flex;gap:6px;align-items:center;">
|
||||
@@ -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 @@
|
||||
? `<span style="color:#3FFF3F;font-size:9px;margin-left:6px;border:1px solid #3FFF3F;border-radius:3px;padding:0 3px;" title="System-Regel — kommt in den System-Export">SYS</span>`
|
||||
: `<span style="color:#FF9F0A;font-size:9px;margin-left:6px;border:1px solid #FF9F0A;border-radius:3px;padding:0 3px;" title="Persönlich — bleibt privat">PRIV</span>`)
|
||||
: '';
|
||||
// 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)
|
||||
? `<div style="color:#555570;font-size:10px;margin-top:2px;">`
|
||||
+ (cMs ? `angelegt ${fmtMemTs(cMs)}` : '')
|
||||
+ (uMs && Math.abs(uMs - cMs) > 60000
|
||||
? ` · geändert ${fmtMemTs(uMs)} <span style="color:#0096FF;">(${fmtMemAgo(uMs)})</span>`
|
||||
: (cMs ? ` <span style="color:#0096FF;">(${fmtMemAgo(cMs)})</span>` : ''))
|
||||
+ `</div>`
|
||||
: '';
|
||||
return `<div style="padding:6px 0;border-bottom:1px solid #1E1E2E;display:flex;gap:6px;align-items:flex-start;">
|
||||
<div style="flex:1;min-width:0;cursor:pointer;" onclick="openMemoryModal('${m.id}')">
|
||||
<div style="color:#E0E0F0;font-size:12px;">${typeBadge}${pin}<strong>${escapeHtml(m.title || '(ohne Titel)')}</strong>${score}${attBadge}${scopeBadge}
|
||||
${m.category ? `<span style="color:#555570;font-weight:normal;font-size:10px;margin-left:6px;">[${escapeHtml(m.category)}]</span>` : ''}
|
||||
</div>
|
||||
<div style="color:#888;font-size:11px;line-height:1.4;">${escapeHtml(preview)}${m.content && m.content.length > 140 ? '...' : ''}</div>
|
||||
${tsLine}
|
||||
</div>
|
||||
<button class="btn secondary" onclick="event.stopPropagation();openMemoryModal('${m.id}')" style="padding:2px 8px;font-size:10px;flex-shrink:0;" title="Bearbeiten">✎</button>
|
||||
<button class="btn secondary" onclick="event.stopPropagation();deleteMemory('${m.id}')" style="padding:2px 8px;font-size:10px;flex-shrink:0;color:#FF6B6B;border-color:#FF6B6B;" title="Loeschen">🗑</button>
|
||||
@@ -6867,6 +7017,71 @@
|
||||
</div>`;
|
||||
}
|
||||
|
||||
/** Sortiert nach der Auswahl im brain-sort-Dropdown. 'type' = unveraendert. */
|
||||
function sortMemories(items) {
|
||||
const mode = document.getElementById('brain-sort')?.value || 'type';
|
||||
if (mode === 'type') return items;
|
||||
const field = mode.startsWith('created') ? 'created' : 'updated';
|
||||
const desc = mode.endsWith('_desc');
|
||||
// Kopie sortieren — die Aufrufer halten teils denselben Array weiter.
|
||||
return items.slice().sort((a, b) => {
|
||||
const d = memTs(a, field) - memTs(b, field);
|
||||
return desc ? -d : d;
|
||||
});
|
||||
}
|
||||
|
||||
/** Zeitraum-Filter aus dem 🕒-Panel. Leere Felder = offenes Ende. */
|
||||
function applyDateFilter(items) {
|
||||
const panel = document.getElementById('brain-date-panel');
|
||||
if (!panel || panel.style.display === 'none') return items;
|
||||
const field = document.getElementById('brain-date-field')?.value || 'updated';
|
||||
const fromRaw = document.getElementById('brain-date-from')?.value || '';
|
||||
const toRaw = document.getElementById('brain-date-to')?.value || '';
|
||||
const from = fromRaw ? Date.parse(fromRaw) : null;
|
||||
const to = toRaw ? Date.parse(toRaw) : null;
|
||||
if (from == null && to == null) return items;
|
||||
return items.filter(m => {
|
||||
const t = memTs(m, field);
|
||||
if (!t) return false; // ohne Zeitstempel nicht in einen Zeitraum einsortierbar
|
||||
if (from != null && t < from) return false;
|
||||
if (to != null && t > to) return false;
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
function toggleDateFilter() {
|
||||
const panel = document.getElementById('brain-date-panel');
|
||||
const btn = document.getElementById('btn-date-filter');
|
||||
if (!panel) return;
|
||||
const open = panel.style.display === 'none';
|
||||
panel.style.display = open ? 'block' : 'none';
|
||||
if (btn) btn.style.color = open ? '#FFD60A' : '#8888AA';
|
||||
if (!open) loadBrainMemoryList(); // zugeklappt = Filter aus
|
||||
}
|
||||
|
||||
function clearDateFilter() {
|
||||
const f = document.getElementById('brain-date-from');
|
||||
const t = document.getElementById('brain-date-to');
|
||||
if (f) f.value = '';
|
||||
if (t) t.value = '';
|
||||
loadBrainMemoryList();
|
||||
}
|
||||
|
||||
/** Schnellwahl: letzte N Tage (0 Uhr bis jetzt). */
|
||||
function setDateQuick(days) {
|
||||
const now = new Date();
|
||||
const from = new Date(now.getTime() - (days - 1) * 86400000);
|
||||
from.setHours(0, 0, 0, 0);
|
||||
const pad = (n) => String(n).padStart(2, '0');
|
||||
const fmt = (d) => `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`
|
||||
+ `T${pad(d.getHours())}:${pad(d.getMinutes())}`;
|
||||
const ef = document.getElementById('brain-date-from');
|
||||
const et = document.getElementById('brain-date-to');
|
||||
if (ef) ef.value = fmt(from);
|
||||
if (et) et.value = fmt(now);
|
||||
loadBrainMemoryList();
|
||||
}
|
||||
|
||||
function renderBrainList(items, isSearchResult) {
|
||||
const el = document.getElementById('brain-memory-list');
|
||||
if (!el) return;
|
||||
@@ -6875,11 +7090,22 @@
|
||||
_updateCategoryDatalist(items);
|
||||
|
||||
if (isSearchResult) {
|
||||
// Such-Treffer: in Aehnlichkeits-Reihenfolge, kein Type-Gruppieren
|
||||
const html = items.map(m => renderMemoryRow(m, true)).join('');
|
||||
// Such-Treffer: Aehnlichkeits-Reihenfolge, ausser es ist bewusst eine
|
||||
// Zeit-Sortierung gewaehlt — dann gilt die auch fuer Treffer.
|
||||
const sorted = sortMemories(items);
|
||||
const html = sorted.map(m => renderMemoryRow(m, true)).join('');
|
||||
el.innerHTML = html || '(Keine Treffer)';
|
||||
return;
|
||||
}
|
||||
// Zeit-Sortierung gewaehlt → flache Liste, keine Typ-Gruppen. Das ist der
|
||||
// Sinn der Sache: man will sehen, was zuletzt passiert ist, nicht in
|
||||
// welche Schublade es gehoert.
|
||||
const sortMode = document.getElementById('brain-sort')?.value || 'type';
|
||||
if (sortMode !== 'type') {
|
||||
const html = sortMemories(items).map(m => renderMemoryRow(m, false)).join('');
|
||||
el.innerHTML = html || '(Keine Memories)';
|
||||
return;
|
||||
}
|
||||
// Normale Liste: nach Type gruppieren, Header klappbar
|
||||
const byType = {};
|
||||
items.forEach(m => { (byType[m.type] = byType[m.type] || []).push(m); });
|
||||
|
||||
@@ -268,6 +268,9 @@ const RUNTIME_CONFIG_FIELDS = [
|
||||
"RVS_HOST", "RVS_PORT", "RVS_TLS", "RVS_TOKEN",
|
||||
"ARIA_AUTH_TOKEN", "WHISPER_MODEL", "WHISPER_LANGUAGE",
|
||||
"brainModel",
|
||||
// Groessen-Limit fuer Memory-Anhaenge in MB. Das Brain liest es pro Upload
|
||||
// aus derselben Datei — Aenderung greift ohne Neustart.
|
||||
"memoryAttachmentMaxMb",
|
||||
];
|
||||
function readRuntimeConfig() {
|
||||
const envDefaults = {
|
||||
@@ -3207,6 +3210,10 @@ wss.on("connection", (ws) => {
|
||||
handleGetModel(ws);
|
||||
} else if (msg.action === "set_model") {
|
||||
handleSetModel(ws, msg.model);
|
||||
} else if (msg.action === "get_attachment_limit") {
|
||||
handleGetAttachmentLimit(ws);
|
||||
} else if (msg.action === "set_attachment_limit") {
|
||||
handleSetAttachmentLimit(ws, msg.maxMb);
|
||||
} else if (msg.action === "voice_id_status") {
|
||||
// An whisper-bridge weiterleiten + Antwort an Browser zurueck
|
||||
const reqId = `vid_${Date.now().toString(36)}`;
|
||||
@@ -3629,6 +3636,76 @@ function handleSetModel(clientWs, model) {
|
||||
return;
|
||||
}
|
||||
|
||||
// ── Einstellungen: Groessen-Limit fuer Memory-Anhaenge ──────────────
|
||||
// Steht in runtime.json als `memoryAttachmentMaxMb`. Das Brain liest es bei
|
||||
// JEDEM Upload neu (memory_attachments.max_bytes()) — eine Aenderung greift
|
||||
// also sofort, ohne aria-brain neu zu starten.
|
||||
//
|
||||
// Obergrenze mit Bedacht: der Anhang geht als base64-in-JSON zur App, die muss
|
||||
// ihn komplett in den Speicher nehmen. Ueber ~64 MB laedt das Handy ihn nicht
|
||||
// mehr (TAP_DL_MAX_BYTES in ChatScreen.tsx) — dann liegt er zwar sauber auf dem
|
||||
// Server, ist mobil aber nur noch per Dateimanager erreichbar. Darum warnen wir
|
||||
// darueber, verbieten es aber nicht.
|
||||
const ATTACHMENT_LIMIT_SOFT_MB = 64;
|
||||
const ATTACHMENT_LIMIT_MAX_MB = 1024;
|
||||
|
||||
function handleGetAttachmentLimit(clientWs) {
|
||||
// Das Brain ist die Wahrheit — es kennt auch den ENV-Fallback, den der
|
||||
// Diagnostic-Container nicht sieht.
|
||||
const brain = process.env.BRAIN_URL || "http://aria-brain:8080";
|
||||
const req = http.get(`${brain}/attachments/limit`, { timeout: 5000 }, (r) => {
|
||||
let body = "";
|
||||
r.on("data", (c) => { body += c; });
|
||||
r.on("end", () => {
|
||||
let maxMb = null;
|
||||
try { maxMb = JSON.parse(body).maxMb; } catch (_) {}
|
||||
clientWs.send(JSON.stringify({
|
||||
type: "attachment_limit_info",
|
||||
maxMb,
|
||||
softMb: ATTACHMENT_LIMIT_SOFT_MB,
|
||||
hardMb: ATTACHMENT_LIMIT_MAX_MB,
|
||||
error: maxMb == null ? "Brain antwortet nicht verwertbar" : undefined,
|
||||
}));
|
||||
});
|
||||
});
|
||||
req.on("error", (err) => {
|
||||
clientWs.send(JSON.stringify({ type: "attachment_limit_info", error: err.message }));
|
||||
});
|
||||
req.on("timeout", () => { req.destroy(new Error("Brain antwortet nicht")); });
|
||||
}
|
||||
|
||||
function handleSetAttachmentLimit(clientWs, maxMb) {
|
||||
const n = Number(maxMb);
|
||||
if (!isFinite(n) || n <= 0) {
|
||||
clientWs.send(JSON.stringify({
|
||||
type: "attachment_limit_info", error: "Bitte eine Zahl groesser 0 angeben (MB)" }));
|
||||
return;
|
||||
}
|
||||
if (n > ATTACHMENT_LIMIT_MAX_MB) {
|
||||
clientWs.send(JSON.stringify({
|
||||
type: "attachment_limit_info",
|
||||
error: `Maximal ${ATTACHMENT_LIMIT_MAX_MB} MB — darueber reisst der WebSocket-Transport` }));
|
||||
return;
|
||||
}
|
||||
try {
|
||||
writeRuntimeConfig({ memoryAttachmentMaxMb: n });
|
||||
log("info", "server", `Anhang-Limit gesetzt: ${n} MB`);
|
||||
clientWs.send(JSON.stringify({
|
||||
type: "attachment_limit_info",
|
||||
maxMb: n,
|
||||
softMb: ATTACHMENT_LIMIT_SOFT_MB,
|
||||
hardMb: ATTACHMENT_LIMIT_MAX_MB,
|
||||
info: n > ATTACHMENT_LIMIT_SOFT_MB
|
||||
? `${n} MB gesetzt (greift sofort). Hinweis: ueber ${ATTACHMENT_LIMIT_SOFT_MB} MB `
|
||||
+ `laedt die Handy-App Anhaenge nicht mehr herunter — sie liegen dann nur `
|
||||
+ `noch auf dem Server.`
|
||||
: `${n} MB gesetzt — greift sofort, kein Neustart noetig.`,
|
||||
}));
|
||||
} catch (err) {
|
||||
clientWs.send(JSON.stringify({ type: "attachment_limit_info", error: err.message }));
|
||||
}
|
||||
}
|
||||
|
||||
// OpenClaw-Config-Handler entfernt — aria-core ist raus.
|
||||
|
||||
// ── Start ───────────────────────────────────────────────
|
||||
|
||||
Reference in New Issue
Block a user