feat: zentrales Pushover-Logging + neuer Admin-Tab Logs
This commit is contained in:
@@ -3185,6 +3185,95 @@ function WebDavSettings({ toast }) {
|
||||
);
|
||||
}
|
||||
|
||||
// Admin: Log aller verschickten Pushover-Nachrichten
|
||||
const PRIORITY_LABELS = { '-2':'Silent', '-1':'Leise', '0':'Normal', '1':'Hoch', '2':'Emergency' };
|
||||
const PRIORITY_COLORS = { '-2':'#888888', '-1':'#4ecdc4', '0':'rgba(255,255,255,0.6)', '1':'#ffe66d', '2':'#ff6b9d' };
|
||||
|
||||
function PushLogs({ toast }) {
|
||||
const [logs, setLogs] = useState([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [filter, setFilter] = useState('');
|
||||
|
||||
const load = () => {
|
||||
setLoading(true);
|
||||
api('/admin/push-logs?limit=300').then(setLogs).catch(e=>toast(e.message,'error')).finally(()=>setLoading(false));
|
||||
};
|
||||
useEffect(() => { load(); }, []);
|
||||
|
||||
const fmtDate = iso => {
|
||||
if (!iso) return '—';
|
||||
const d = new Date(iso.replace(' ', 'T'));
|
||||
if (Number.isNaN(d.getTime())) return iso;
|
||||
return d.toLocaleString('de-DE', { day:'2-digit', month:'2-digit', year:'2-digit', hour:'2-digit', minute:'2-digit', second:'2-digit' });
|
||||
};
|
||||
|
||||
const filtered = filter.trim()
|
||||
? logs.filter(l => (l.username||'').toLowerCase().includes(filter.toLowerCase())
|
||||
|| (l.title||'').toLowerCase().includes(filter.toLowerCase())
|
||||
|| (l.message||'').toLowerCase().includes(filter.toLowerCase())
|
||||
|| (l.source||'').toLowerCase().includes(filter.toLowerCase()))
|
||||
: logs;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Sec title={`PUSHOVER-VERLAUF (${filtered.length})`}>
|
||||
<div style={{ display:'flex', gap:8, marginBottom:12 }}>
|
||||
<input value={filter} onChange={e=>setFilter(e.target.value)}
|
||||
placeholder="Filtern nach Benutzer, Text, Quelle…"
|
||||
style={{ ...S.inp, flex:1 }} />
|
||||
<button onClick={load} style={{ ...S.btn('#4ecdc4'), flexShrink:0 }}>↻</button>
|
||||
</div>
|
||||
|
||||
{loading ? (
|
||||
<div style={{ color:'rgba(255,255,255,0.3)', fontFamily:'monospace', fontSize:12 }}>Lädt…</div>
|
||||
) : filtered.length === 0 ? (
|
||||
<div style={{ color:'rgba(255,255,255,0.3)', fontFamily:'monospace', fontSize:12 }}>Keine Einträge.</div>
|
||||
) : (
|
||||
<div style={{ display:'flex', flexDirection:'column', gap:6, maxHeight:'65vh', overflowY:'auto' }}>
|
||||
{filtered.map(l => (
|
||||
<div key={l.id} style={{
|
||||
border:'1px solid rgba(255,255,255,0.08)', borderRadius:8, padding:'8px 10px',
|
||||
background: l.success ? 'transparent' : 'rgba(248,113,113,0.06)',
|
||||
}}>
|
||||
<div style={{ display:'flex', justifyContent:'space-between', alignItems:'baseline', gap:8, marginBottom:3, flexWrap:'wrap' }}>
|
||||
<span style={{ color:'#4ecdc4', fontFamily:"'Space Mono',monospace", fontSize:11, fontWeight:700 }}>
|
||||
{l.username || `User #${l.user_id ?? '?'}`}
|
||||
</span>
|
||||
<span style={{ color:'rgba(255,255,255,0.35)', fontFamily:'monospace', fontSize:10 }}>
|
||||
{fmtDate(l.created_at)}
|
||||
</span>
|
||||
</div>
|
||||
<div style={{ color:'rgba(255,255,255,0.85)', fontFamily:'monospace', fontSize:12, marginBottom:2 }}>
|
||||
{l.title}
|
||||
</div>
|
||||
<div style={{ color:'rgba(255,255,255,0.5)', fontFamily:'monospace', fontSize:11, marginBottom:6, whiteSpace:'pre-wrap' }}>
|
||||
{l.message}
|
||||
</div>
|
||||
<div style={{ display:'flex', gap:8, flexWrap:'wrap', alignItems:'center' }}>
|
||||
<span style={{
|
||||
color: PRIORITY_COLORS[String(l.priority)] || 'rgba(255,255,255,0.5)',
|
||||
fontFamily:'monospace', fontSize:9, letterSpacing:1,
|
||||
border:`1px solid ${PRIORITY_COLORS[String(l.priority)] || 'rgba(255,255,255,0.2)'}`,
|
||||
borderRadius:4, padding:'1px 6px',
|
||||
}}>
|
||||
{PRIORITY_LABELS[String(l.priority)] || l.priority}
|
||||
</span>
|
||||
<span style={{ color:'rgba(255,255,255,0.3)', fontFamily:'monospace', fontSize:9 }}>
|
||||
{l.source || '—'}
|
||||
</span>
|
||||
{!l.success && (
|
||||
<span style={{ color:'#f87171', fontFamily:'monospace', fontSize:9 }}>✕ fehlgeschlagen</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</Sec>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function AdminPanel({ toast, mobile, user, nav }) {
|
||||
const [section, setSection] = useState('profil');
|
||||
useEffect(() => { if (nav?.section) setSection(nav.section); }, [nav?.ts]);
|
||||
@@ -3246,7 +3335,7 @@ function AdminPanel({ toast, mobile, user, nav }) {
|
||||
</div>
|
||||
|
||||
<div style={{ display:'flex', gap:6, marginBottom:18, flexWrap:'wrap' }}>
|
||||
{[['profil','Profil'],['sicherheit','Sicherheit'],['dashboard','Dashboard'], ...(user?.role==='admin'?[['benutzer','Benutzer'],['backup','Backup']]:[])]
|
||||
{[['profil','Profil'],['sicherheit','Sicherheit'],['dashboard','Dashboard'], ...(user?.role==='admin'?[['benutzer','Benutzer'],['backup','Backup'],['logs','Logs']]:[])]
|
||||
.map(([k,l]) => (
|
||||
<button key={k} onClick={()=>setSection(k)} style={{
|
||||
padding:'6px 16px', borderRadius:20, fontFamily:'monospace', fontSize:12, cursor:'pointer',
|
||||
@@ -3382,6 +3471,10 @@ function AdminPanel({ toast, mobile, user, nav }) {
|
||||
</Sec>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{section === 'logs' && user?.role==='admin' && (
|
||||
<PushLogs toast={toast}/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user