/** * Trigger-Browser — Liste aller Trigger (timer + watcher) mit Toggle, * Tap-zum-Bearbeiten und "+ Neu"-Knopf. * * Eingesetzt von SettingsScreen → Sektion "Trigger". * * Brain-API ueber brainApi (RVS-Brain-Proxy). */ import React, { useCallback, useEffect, useState } from 'react'; import { ActivityIndicator, Alert, FlatList, Modal, ScrollView, StyleSheet, Switch, Text, TextInput, TouchableOpacity, View, } from 'react-native'; import brainApi, { Trigger } from '../services/brainApi'; const COL_ACTIVE = '#34C759'; const COL_INACTIVE = '#555570'; const COL_TIMER = '#0096FF'; const COL_WATCHER = '#FFD60A'; function relTime(iso: string | null | undefined): string { if (!iso) return '—'; const t = new Date(iso).getTime(); if (!t) return '—'; const diffSec = Math.floor((Date.now() - t) / 1000); if (diffSec < 60) return `vor ${diffSec}s`; if (diffSec < 3600) return `vor ${Math.floor(diffSec / 60)}min`; if (diffSec < 86400) return `vor ${Math.floor(diffSec / 3600)}h`; return `vor ${Math.floor(diffSec / 86400)}d`; } export const TriggerBrowser: React.FC = () => { const [items, setItems] = useState([]); const [loading, setLoading] = useState(false); const [err, setErr] = useState(null); const [filter, setFilter] = useState<'all' | 'active' | 'inactive'>('all'); const [editTrigger, setEditTrigger] = useState(null); const [showNew, setShowNew] = useState(false); const load = useCallback(() => { setLoading(true); setErr(null); brainApi.listTriggers() .then(t => { // Sortierung: aktive zuerst, dann nach Name t.sort((a, b) => { if (a.active !== b.active) return a.active ? -1 : 1; return (a.name || '').localeCompare(b.name || ''); }); setItems(t); }) .catch(e => setErr(String(e?.message || e))) .finally(() => setLoading(false)); }, []); useEffect(() => { load(); }, [load]); const visible = items.filter(t => { if (filter === 'active') return t.active; if (filter === 'inactive') return !t.active; return true; }); const toggleActive = (t: Trigger) => { brainApi.updateTrigger(t.name, { active: !t.active }) .then(() => load()) .catch(e => Alert.alert('Fehler', String(e?.message || e))); }; const deleteTrigger = (t: Trigger) => { Alert.alert( 'Trigger löschen?', `"${t.name}" — diese Aktion ist nicht rückgängig zu machen.`, [ { text: 'Abbrechen', style: 'cancel' }, { text: 'Löschen', style: 'destructive', onPress: () => { brainApi.deleteTrigger(t.name) .then(() => { setEditTrigger(null); load(); }) .catch(e => Alert.alert('Fehler', String(e?.message || e))); }, }, ], ); }; const renderItem = ({ item }: { item: Trigger }) => { const typeColor = item.type === 'timer' ? COL_TIMER : COL_WATCHER; const typeLabel = item.type === 'timer' ? '⏰ Timer' : '👁 Watcher'; return ( setEditTrigger(item)}> {typeLabel} {item.name} {item.message} {item.type === 'watcher' && item.condition ? ( {item.condition} ) : null} {item.type === 'timer' && item.fires_at ? ( feuert: {new Date(item.fires_at).toLocaleString('de-DE')} ) : null} {item.fire_count || 0}× gefeuert · zuletzt: {relTime(item.last_fired_at)} toggleActive(item)} trackColor={{ false: '#1E1E2E', true: COL_ACTIVE }} thumbColor="#E0E0F0" /> ); }; return ( {/* Filter-Leiste + Reload + Neu */} {(['all', 'active', 'inactive'] as const).map(f => ( setFilter(f)} > {f === 'all' ? 'Alle' : f === 'active' ? 'Aktive' : 'Inaktive'} ))} {'↻'} setShowNew(true)} style={[s.iconBtn, {backgroundColor: '#0096FF'}]}> + Neu {err ? {err} : null} {loading && items.length === 0 ? ( ) : ( t.name} renderItem={renderItem} nestedScrollEnabled={true} ListEmptyComponent={ {items.length === 0 ? '(keine Trigger angelegt)' : '(keine Treffer für diesen Filter)'} } contentContainerStyle={{paddingBottom: 20}} /> )} {editTrigger ? ( setEditTrigger(null)} onSaved={() => { setEditTrigger(null); load(); }} onDelete={() => deleteTrigger(editTrigger)} /> ) : null} {showNew ? ( setShowNew(false)} onCreated={() => { setShowNew(false); load(); }} /> ) : null} ); }; // ── Edit-Modal ───────────────────────────────────────────────────────── interface EditProps { trigger: Trigger; onClose: () => void; onSaved: () => void; onDelete: () => void; } const TriggerEditModal: React.FC = ({ trigger, onClose, onSaved, onDelete }) => { const [message, setMessage] = useState(trigger.message || ''); const [condition, setCondition] = useState(trigger.condition || ''); const [firesAt, setFiresAt] = useState(trigger.fires_at || ''); const [checkInterval, setCheckInterval] = useState(String(trigger.check_interval_sec || 300)); const [throttle, setThrottle] = useState(String(trigger.throttle_sec || 3600)); const [saving, setSaving] = useState(false); const save = () => { setSaving(true); const patch: any = { message }; if (trigger.type === 'watcher') { patch.condition = condition; patch.check_interval_sec = parseInt(checkInterval, 10) || 300; patch.throttle_sec = parseInt(throttle, 10) || 3600; } else if (trigger.type === 'timer') { patch.fires_at = firesAt; } brainApi.updateTrigger(trigger.name, patch) .then(onSaved) .catch(e => Alert.alert('Fehler beim Speichern', String(e?.message || e))) .finally(() => setSaving(false)); }; return ( {trigger.type === 'timer' ? '⏰' : '👁'} {trigger.name} × Nachricht {trigger.type === 'watcher' ? ( <> Condition Check-Intervall (s) Throttle (s) ) : ( <> Feuert am (ISO, UTC) )} Status: {trigger.active ? '🟢 aktiv' : '⚪ inaktiv'} Gefeuert: {trigger.fire_count || 0}× Zuletzt gefeuert: {relTime(trigger.last_fired_at)} Zuletzt geprüft: {relTime(trigger.last_checked_at)} {trigger.author ? Angelegt von: {trigger.author} : null} 🗑 Löschen {saving ? 'Speichert...' : 'Speichern'} ); }; // ── Neu-Modal ────────────────────────────────────────────────────────── interface NewProps { onClose: () => void; onCreated: () => void; } const TriggerNewModal: React.FC = ({ onClose, onCreated }) => { const [ttype, setTtype] = useState<'timer' | 'watcher'>('watcher'); const [name, setName] = useState(''); const [message, setMessage] = useState(''); const [condition, setCondition] = useState(''); const [firesAt, setFiresAt] = useState(''); const [checkInterval, setCheckInterval] = useState('300'); const [throttle, setThrottle] = useState('3600'); const [saving, setSaving] = useState(false); const create = () => { if (!name.trim() || !message.trim()) { Alert.alert('Name und Nachricht erforderlich'); return; } setSaving(true); const promise = ttype === 'timer' ? brainApi.createTimer({ name: name.trim(), fires_at: firesAt.trim(), message: message.trim(), }) : brainApi.createWatcher({ name: name.trim(), condition: condition.trim(), message: message.trim(), check_interval_sec: parseInt(checkInterval, 10) || 300, throttle_sec: parseInt(throttle, 10) || 3600, }); promise .then(onCreated) .catch(e => Alert.alert('Fehler beim Anlegen', String(e?.message || e))) .finally(() => setSaving(false)); }; return ( + Neuer Trigger × Typ {(['watcher', 'timer'] as const).map(t => ( setTtype(t)} style={[s.chip, ttype === t && s.chipActive, {flex: 1, paddingVertical: 10}]} > {t === 'watcher' ? '👁 Watcher' : '⏰ Timer'} ))} Name (kebab-case) Nachricht {ttype === 'watcher' ? ( <> Condition Funktionen: near() / entered_near() / left_near() · Variablen: disk_free_gb, hour_of_day, current_lat, current_lon, last_user_message_ago_sec Check-Intervall (s) Throttle (s) ) : ( <> Feuert am (ISO, UTC) Beispiel oben: heute 20:00 UTC = 22:00 CEST )} {saving ? 'Legt an...' : 'Anlegen'} ); }; const s = StyleSheet.create({ toolbar: { flexDirection: 'row', alignItems: 'center', gap: 6, marginBottom: 8, }, chip: { paddingHorizontal: 10, paddingVertical: 6, borderRadius: 14, backgroundColor: '#1E1E2E', }, chipActive: { backgroundColor: '#FFD60A', }, iconBtn: { paddingHorizontal: 10, paddingVertical: 6, borderRadius: 14, backgroundColor: '#1E1E2E', }, err: { color: '#FF3B30', padding: 12, fontSize: 12, }, row: { flexDirection: 'row', alignItems: 'center', padding: 12, backgroundColor: '#1A1A2E', borderRadius: 8, marginBottom: 6, }, modalBg: { flex: 1, backgroundColor: 'rgba(0,0,0,0.6)', justifyContent: 'center', alignItems: 'center', padding: 16, }, modal: { backgroundColor: '#0D0D1A', borderRadius: 12, width: '100%', maxWidth: 600, maxHeight: '90%', borderWidth: 1, borderColor: '#1E1E2E', }, modalHeader: { flexDirection: 'row', alignItems: 'center', padding: 14, borderBottomWidth: 1, borderBottomColor: '#1E1E2E', }, modalFooter: { flexDirection: 'row', alignItems: 'center', padding: 12, borderTopWidth: 1, borderTopColor: '#1E1E2E', gap: 8, }, label: { color: '#8888AA', fontSize: 11, fontWeight: '700', textTransform: 'uppercase', letterSpacing: 0.5, marginTop: 8, marginBottom: 4, }, input: { backgroundColor: '#1A1A2E', borderWidth: 1, borderColor: '#1E1E2E', borderRadius: 6, color: '#E0E0F0', padding: 10, fontSize: 14, marginBottom: 8, }, hint: { color: '#555570', fontSize: 11, fontStyle: 'italic', marginTop: -4, marginBottom: 10, }, metaBox: { backgroundColor: '#1A1A2E', borderRadius: 6, padding: 10, marginTop: 10, gap: 4, }, meta: { color: '#8888AA', fontSize: 12, }, btn: { paddingHorizontal: 14, paddingVertical: 10, borderRadius: 6, borderWidth: 1, borderColor: 'transparent', }, }); export default TriggerBrowser;