fix(app): Inbox Scroll-Bug + feat(diagnostic): Trigger-Edit

App-Inbox-Modal:
- ScrollView der Top-Section ('Aus diesem Chat') nestedScrollEnabled=true
- MemoryBrowser darunter in einen flex:1-Wrapper gepackt damit er den
  verbleibenden Platz bekommt — ohne den hat seine FlatList intern
  null Hoehe gehabt und Scroll-Gestures verschluckt.

Diagnostic Trigger-Tab:
- ✎ Bearbeiten-Knopf pro Zeile (neben Aktivieren/Deaktivieren/Loeschen)
- Modal hat jetzt einen Edit-Modus: Type+Name disabled, Save-Button
  zeigt 'Speichern', Modal-Title 'Trigger bearbeiten — <name>'
- Fuer Timer im Edit-Modus ein zusaetzliches Feld 'Feuert am (ISO, UTC)'
  damit man den absoluten Zeitpunkt direkt aendern kann (statt 'in X
  Minuten ab jetzt' das nur fuer Create Sinn macht)
- saveTrigger() unterscheidet jetzt zwischen Create-Modus (POST
  /triggers/timer|watcher) und Edit-Modus (PATCH /triggers/{name})
- openTriggerEdit(name) fuellt das Modal mit Werten aus dem Cache

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-15 22:49:51 +02:00
parent 7093ebaf0b
commit 03edee8881
2 changed files with 105 additions and 7 deletions
+7 -2
View File
@@ -2539,7 +2539,7 @@ const ChatScreen: React.FC = () => {
<Text style={{color:'#8888AA', fontSize:11, paddingHorizontal:14, paddingTop:8, paddingBottom:4, textTransform:'uppercase', letterSpacing:0.5}}>
Aus diesem Chat
</Text>
<ScrollView style={{paddingHorizontal:8}}>
<ScrollView style={{paddingHorizontal:8}} nestedScrollEnabled={true}>
{specials.map(m => {
if (m.memorySaved) {
const ms = m.memorySaved;
@@ -2595,7 +2595,12 @@ const ChatScreen: React.FC = () => {
<Text style={{color:'#8888AA', fontSize:11, paddingHorizontal:14, paddingTop:10, paddingBottom:4, textTransform:'uppercase', letterSpacing:0.5}}>
Alle Memories aus der DB
</Text>
<MemoryBrowser onOpenMemory={(id) => { setInboxVisible(false); setMemoryDetailId(id); }} />
{/* flex:1 Wrapper damit MemoryBrowser den verbleibenden Platz
bekommt (sonst rendert die FlatList intern mit 0 Hoehe und
nimmt nur was der Inhalt sagt → Scroll-Gestures verschwinden). */}
<View style={{flex:1}}>
<MemoryBrowser onOpenMemory={(id) => { setInboxVisible(false); setMemoryDetailId(id); }} />
</View>
</View>
</ErrorBoundary>
</Modal>
+98 -5
View File
@@ -968,11 +968,11 @@
</div>
</div><!-- /tab-triggers -->
<!-- Trigger-Create Modal -->
<!-- Trigger-Create/Edit Modal -->
<div class="modal-overlay" id="trigger-modal">
<div class="modal-box" style="max-width:600px;">
<div class="modal-header">
<h3>Neuer Trigger</h3>
<h3 id="trigger-modal-title">Neuer Trigger</h3>
<button class="modal-close" onclick="closeTriggerModal()">&times;</button>
</div>
<div class="modal-body" style="padding:16px;">
@@ -986,8 +986,16 @@
<!-- Timer-spezifisch -->
<div id="trigger-timer-fields">
<label style="display:block;font-size:11px;color:#8888AA;margin-bottom:4px;">In wievielen Minuten?</label>
<input type="number" id="trigger-timer-minutes" min="1" max="10080" value="10" style="width:100%;background:#0D0D1A;color:#E0E0F0;border:1px solid #1E1E2E;padding:6px;border-radius:4px;font-family:inherit;margin-bottom:10px;">
<!-- Create-mode: relativ („in X Minuten ab jetzt") -->
<div id="trigger-timer-create-fields">
<label style="display:block;font-size:11px;color:#8888AA;margin-bottom:4px;">In wievielen Minuten?</label>
<input type="number" id="trigger-timer-minutes" min="1" max="10080" value="10" style="width:100%;background:#0D0D1A;color:#E0E0F0;border:1px solid #1E1E2E;padding:6px;border-radius:4px;font-family:inherit;margin-bottom:10px;">
</div>
<!-- Edit-mode: absoluter ISO-Timestamp (UTC) -->
<div id="trigger-timer-edit-fields" style="display:none;">
<label style="display:block;font-size:11px;color:#8888AA;margin-bottom:4px;">Feuert am (ISO, UTC)</label>
<input type="text" id="trigger-timer-fires-at" placeholder="2026-05-15T20:00:00+00:00" style="width:100%;background:#0D0D1A;color:#E0E0F0;border:1px solid #1E1E2E;padding:6px;border-radius:4px;font-family:monospace;margin-bottom:10px;">
</div>
</div>
<!-- Watcher-spezifisch -->
@@ -1008,7 +1016,7 @@
</div>
<div class="modal-footer" style="padding:10px 16px;border-top:1px solid #1E1E2E;display:flex;justify-content:flex-end;gap:8px;">
<button class="btn secondary" onclick="closeTriggerModal()">Abbrechen</button>
<button class="btn" onclick="saveTrigger()">Anlegen</button>
<button class="btn" id="trigger-modal-save-btn" onclick="saveTrigger()">Anlegen</button>
</div>
</div>
</div>
@@ -3101,6 +3109,7 @@
<div style="color:#8888AA;font-size:11px;margin-top:4px;">${detailLine}</div>
<div style="color:#888;font-size:12px;margin-top:2px;">"${escapeHtml(t.message || '')}"</div>
<div style="margin-top:6px;display:flex;gap:6px;">
<button class="btn secondary" onclick="openTriggerEdit('${escapeHtml(t.name)}')" style="padding:2px 10px;font-size:10px;color:#0096FF;border-color:#0096FF;">✎ Bearbeiten</button>
<button class="btn secondary" onclick="toggleTriggerActive('${escapeHtml(t.name)}', ${!active})" style="padding:2px 10px;font-size:10px;color:#FF9500;border-color:#FF9500;">${active ? '⏸ Deaktivieren' : '▶ Aktivieren'}</button>
<button class="btn secondary" onclick="deleteTrigger('${escapeHtml(t.name)}')" style="padding:2px 10px;font-size:10px;color:#FF6B6B;border-color:#FF6B6B;">🗑 Löschen</button>
</div>
@@ -3138,10 +3147,21 @@
document.getElementById('trigger-watcher-fields').style.display = t === 'watcher' ? '' : 'none';
}
// null = Create-Modus, string = Edit-Modus (Name der bearbeiteten Bubble)
let editingTriggerName = null;
async function openTriggerCreate() {
editingTriggerName = null;
document.getElementById('trigger-modal-title').textContent = 'Neuer Trigger';
document.getElementById('trigger-modal-save-btn').textContent = 'Anlegen';
document.getElementById('trigger-type').disabled = false;
document.getElementById('trigger-name').disabled = false;
document.getElementById('trigger-timer-create-fields').style.display = '';
document.getElementById('trigger-timer-edit-fields').style.display = 'none';
document.getElementById('trigger-type').value = 'timer';
document.getElementById('trigger-name').value = '';
document.getElementById('trigger-timer-minutes').value = '10';
document.getElementById('trigger-timer-fires-at').value = '';
document.getElementById('trigger-condition').value = '';
document.getElementById('trigger-check-interval').value = '300';
document.getElementById('trigger-throttle').value = '3600';
@@ -3170,6 +3190,52 @@
function closeTriggerModal() {
document.getElementById('trigger-modal').classList.remove('open');
editingTriggerName = null;
}
/** Edit-Modus: Modal mit existierenden Trigger-Werten fuellen. */
async function openTriggerEdit(name) {
const t = triggersCache.find(x => x.name === name);
if (!t) { alert('Trigger nicht in cache, lade neu...'); loadTriggers(); return; }
editingTriggerName = name;
document.getElementById('trigger-modal-title').textContent = 'Trigger bearbeiten — ' + name;
document.getElementById('trigger-modal-save-btn').textContent = 'Speichern';
// Type + Name sind im Edit-Modus nicht aenderbar
document.getElementById('trigger-type').value = t.type;
document.getElementById('trigger-type').disabled = true;
document.getElementById('trigger-name').value = t.name;
document.getElementById('trigger-name').disabled = true;
// Timer: relative-Minutes-Feld aus, absoluter ISO-Feld an
document.getElementById('trigger-timer-create-fields').style.display = 'none';
document.getElementById('trigger-timer-edit-fields').style.display = '';
document.getElementById('trigger-timer-fires-at').value = t.fires_at || '';
// Watcher-Felder vorbefuellen
document.getElementById('trigger-condition').value = t.condition || '';
document.getElementById('trigger-check-interval').value = String(t.check_interval_sec || 300);
document.getElementById('trigger-throttle').value = String(t.throttle_sec || 3600);
document.getElementById('trigger-message').value = t.message || '';
document.getElementById('trigger-modal-error').style.display = 'none';
onTriggerTypeChange();
// Variablen-Hinweis fuer Watcher auch im Edit-Modus
if (t.type === 'watcher') {
try {
const r = await fetch('/api/brain/triggers/conditions');
const d = await r.json();
const info = document.getElementById('trigger-vars-info');
if (info) {
const vars = (d.variables || []).map(v =>
`<code>${escapeHtml(v.name)}</code>=${escapeHtml(String(d.current[v.name]))} <span style="color:#444;">(${escapeHtml(v.desc)})</span>`
).join(' · ');
const fns = (d.functions || []).map(f =>
`<code>${escapeHtml(f.signature)}</code> — ${escapeHtml(f.desc)}`
).join('<br>');
info.innerHTML =
'<strong>Variablen:</strong> ' + vars +
(fns ? '<br><br><strong>Funktionen:</strong><br>' + fns : '');
}
} catch {}
}
document.getElementById('trigger-modal').classList.add('open');
}
async function saveTrigger() {
@@ -3181,6 +3247,33 @@
if (!name) { errEl.textContent = 'Name fehlt.'; errEl.style.display = 'block'; return; }
if (!message) { errEl.textContent = 'Nachricht fehlt.'; errEl.style.display = 'block'; return; }
try {
// ── EDIT-MODUS ──────────────────────────────────────────
if (editingTriggerName) {
const patch = { message };
if (ttype === 'watcher') {
const condition = document.getElementById('trigger-condition').value.trim();
if (!condition) { errEl.textContent = 'Condition fehlt.'; errEl.style.display = 'block'; return; }
patch.condition = condition;
patch.check_interval_sec = parseInt(document.getElementById('trigger-check-interval').value, 10) || 300;
patch.throttle_sec = parseInt(document.getElementById('trigger-throttle').value, 10) || 3600;
} else if (ttype === 'timer') {
const fa = document.getElementById('trigger-timer-fires-at').value.trim();
if (fa) patch.fires_at = fa;
}
const r = await fetch('/api/brain/triggers/' + encodeURIComponent(editingTriggerName), {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(patch),
});
if (!r.ok) {
const txt = await r.text();
throw new Error('HTTP ' + r.status + ': ' + txt.slice(0, 200));
}
closeTriggerModal();
loadTriggers();
return;
}
// ── CREATE-MODUS ────────────────────────────────────────
let url, body;
if (ttype === 'timer') {
const mins = parseInt(document.getElementById('trigger-timer-minutes').value, 10) || 10;