Dashboard
@@ -846,10 +997,22 @@ function Dashboard({ toast, mobile, setActive, unreadMsgs=0 }) {
-
- {typeof __BUILD_TIME__ !== 'undefined' && __BUILD_TIME__
- ? `Letzte Versionierung: ${__BUILD_TIME__}` : null}
+
+
+ {typeof __BUILD_TIME__ !== 'undefined' && __BUILD_TIME__
+ ? `Letzte Versionierung: ${__BUILD_TIME__}` : null}
+
+
setShowBoard(true)} style={{
+ position:'relative', background:'rgba(255,255,255,0.04)',
+ border:'1px solid rgba(255,255,255,0.1)', borderRadius:7,
+ color:'rgba(255,255,255,0.45)', cursor:'pointer',
+ padding:'4px 10px', fontSize:9, fontFamily:'monospace', letterSpacing:1 }}>
+ 📋 ToDo
+ {boardUnread > 0 && (
+
+ )}
+
);
@@ -1607,7 +1770,8 @@ export default function App() {
const [toastMsg,setToastMsg]=useState({msg:'',type:'success'});
const [updateInfo,setUpdateInfo]=useState(null);
const [showUpd,setShowUpd]=useState(false);
- const [unreadMsgs,setUnreadMsgs]=useState(0);
+ const [unreadMsgs, setUnreadMsgs] = useState(0);
+ const [unreadBoard, setUnreadBoard] = useState(0);
const [authChecked, setAuthChecked] = useState(false);
useEffect(()=>{
@@ -1640,6 +1804,14 @@ export default function App() {
}).catch(()=>{});
},[user]);
+ useEffect(()=>{
+ if(!user)return;
+ const poll=()=>api('/dashboard/board/unread').then(d=>setUnreadBoard(d.unread||0)).catch(()=>{});
+ poll();
+ const t=setInterval(poll,60*1000);
+ return()=>clearInterval(t);
+ },[user]);
+
useEffect(()=>{
if(!user)return;
const poll=()=>api('/tools/nachrichten/unread').then(d=>setUnreadMsgs(d.count||0)).catch(()=>{});
@@ -1687,7 +1859,7 @@ export default function App() {
unreadMsgs={unreadMsgs}/>
)}
- {active==='dashboard' && }
+ {active==='dashboard' && }
{active==='admin' && }
{activeTool && }
diff --git a/frontend/src/tools/App.jsx b/frontend/src/tools/App.jsx
deleted file mode 100644
index cbb67a1..0000000
--- a/frontend/src/tools/App.jsx
+++ /dev/null
@@ -1,1607 +0,0 @@
-import { useState, useEffect, useRef, useCallback } from 'react';
-import { api, S } from './lib.js';
-import { TOOLS, getGroupedTools } from './toolRegistry.js';
-import CalendarWidget, { CalendarSettings } from './calendar.jsx';
-import { DateiSettings } from './tools/dateien.jsx';
-import { useConfirm } from './confirm.jsx';
-import { HomeIcon, AppsIcon, AdminIcon, MoreIcon, UserIcon, LogOutIcon, ChevronIcon, UpdateIcon, TrashIcon, MessageIcon } from './icons.jsx';
-import { getOrCreateKeyPair, getPublicKeyJwk, getFingerprint, exportEncrypted, importEncrypted, generateNewKeyPair } from './crypto.js';
-
-const CONST_ICONS = ['🔗','🌐','📊','📁','🛠','📧','🗓','💾','🖥','📡','🔒','📖','🎯','⚡','🏠','🔧','📱','🎮','🚀','💡'];
-
-// Schlüsselpaar beim Login generieren und public key registrieren (fire & forget)
-async function initNachrichtenKeys() {
- const LS_KEY = 'dd_msg_keypair';
- let pubJwk;
- const stored = localStorage.getItem(LS_KEY);
- if (stored) {
- try { pubJwk = JSON.parse(stored).pub; } catch { localStorage.removeItem(LS_KEY); }
- }
- if (!pubJwk) {
- const kp = await crypto.subtle.generateKey({ name:'ECDH', namedCurve:'P-256' }, true, ['deriveKey']);
- pubJwk = await crypto.subtle.exportKey('jwk', kp.publicKey);
- const priv = await crypto.subtle.exportKey('jwk', kp.privateKey);
- localStorage.setItem(LS_KEY, JSON.stringify({ pub: pubJwk, priv }));
- }
- try { await api('/tools/nachrichten/keys', { body: { public_key: JSON.stringify(pubJwk) } }); } catch {}
-}
-
-// ── Responsive Hook ───────────────────────────────────────────────────────────
-function useIsMobile() {
- const [mobile, setMobile] = useState(window.innerWidth < 768);
- useEffect(() => {
- // Nur auf Breite reagieren, nicht auf Keyboard-Open (Höhenänderung)
- const fn = () => {
- const isMob = window.innerWidth < 768;
- setMobile(prev => prev === isMob ? prev : isMob);
- };
- window.addEventListener('resize', fn);
- return () => window.removeEventListener('resize', fn);
- }, []);
- return mobile;
-}
-
-// ── Toast ─────────────────────────────────────────────────────────────────────
-function Toast({ msg, type }) {
- if (!msg) return null;
- const c = type === 'error' ? '#ff6b9d' : '#4ecdc4';
- return (
-
- {type==='error'?'✕ ':'✓ '}{msg}
-
- );
-}
-
-// ── Modal ─────────────────────────────────────────────────────────────────────
-function Modal({ title, onClose, children }) {
- return (
-
=768 ? 'center' : 'flex-end',
- justifyContent:'center', padding: window.innerWidth>=768 ? 24 : 0 }}
- onClick={e=>e.target===e.currentTarget&&onClose()}>
-
=768 ? 16 : '16px 16px 0 0',
- padding:'20px 20px 40px', width:'100%',
- maxWidth:600, maxHeight:'85vh', overflowY:'auto',
- boxShadow: window.innerWidth>=768 ? '0 24px 80px rgba(0,0,0,0.6)' : 'none' }}>
- {window.innerWidth < 768 &&
}
-
- {title}
- ✕
-
- {children}
-
-
- );
-}
-
-// ── PWA Splash Screen ─────────────────────────────────────────────────────────
-function SplashScreen({ onDone }) {
- const [fade, setFade] = useState(false);
- useEffect(() => {
- const t1 = setTimeout(() => setFade(true), 1200);
- const t2 = setTimeout(() => onDone(), 1700);
- return () => { clearTimeout(t1); clearTimeout(t2); };
- }, []);
- return (
-
- );
-}
-
-// ── Login ─────────────────────────────────────────────────────────────────────
-function Login({ onLogin }) {
- const [u,setU]=useState(''); const [p,setP]=useState('');
- const [err,setErr]=useState(''); const [busy,setBusy]=useState(false);
- const go = async () => {
- setErr(''); setBusy(true);
- try { const d=await api('/auth/login',{body:{username:u,password:p}}); localStorage.setItem('sk_token',d.token); initNachrichtenKeys(); onLogin(d.user); }
- catch(e) { setErr(e.message); } finally { setBusy(false); }
- };
- return (
-
-
-
- {[['BENUTZERNAME',u,setU,'text'],['PASSWORT',p,setP,'password']].map(([l,v,s,t])=>(
-
- {l}
- s(e.target.value)}
- onKeyDown={e=>e.key==='Enter'&&go()}
- style={{ ...S.inp, fontSize:16, padding:'13px 14px', borderRadius:10,
- border:'1px solid rgba(255,255,255,0.12)' }}
- autoCapitalize="none" autoCorrect="off" />
-
- ))}
- {err &&
✕ {err}
}
-
{busy?'…':'Einloggen →'}
-
-
- );
-}
-
-// ── Update Modal ──────────────────────────────────────────────────────────────
-function UpdateModal({ data, onClose }) {
- return (
-
-
-
- Installiert: {data.currentVersion}
- {' → '}{data.latestVersion}
-
-
- {(data.releases||[]).map((r,i)=>(
-
-
- {r.name||r.version}
-
- {r.publishedAt?new Date(r.publishedAt).toLocaleDateString('de-DE'):''}
-
-
-
{r.body}
-
- ))}
-
- Update: docker compose up -d --build
-
-
- );
-}
-
-// ── Desktop Sidebar ───────────────────────────────────────────────────────────
-function Sidebar({ active, setActive, user, onLogout, updateInfo, onUpdateClick, unreadMsgs=0 }) {
- const [appsOpen, setAppsOpen] = useState(true);
- const [collapsed, setCollapsed] = useState(false);
-
- const NavBtn = ({ item, sub=false }) => {
- const ic = active===item.id ? '#4ecdc4' : 'rgba(255,255,255,0.55)';
- return (
-
setActive(item.id)} title={collapsed ? item.label : undefined} style={{
- width:'100%', display:'flex', alignItems:'center', gap:9,
- padding: collapsed ? '8px 0' : '8px 12px',
- justifyContent: collapsed ? 'center' : 'flex-start',
- borderRadius:7, border:'none',
- background: active===item.id ? 'linear-gradient(90deg,rgba(78,205,196,0.14),rgba(78,205,196,0.02))' : 'transparent',
- cursor:'pointer', fontSize:12,
- fontFamily:"'Space Mono',monospace", textAlign:'left', marginBottom:1,
- borderLeft: active===item.id ? '2px solid #4ecdc4' : '2px solid transparent',
- position:'relative',
- }}>
- {!collapsed && sub && }
-
- {item.Icon ? (() => { const I = item.Icon; return ; })() : {item.icon} }
- {collapsed && item.id==='nachrichten' && unreadMsgs>0 && (
-
- )}
-
- {!collapsed && {item.label} }
- {!collapsed && item.id==='nachrichten' && unreadMsgs>0 && (
- {unreadMsgs}
- )}
-
- );
- };
-
- return (
-
-
- {collapsed ? (
-
-
⚒
-
setCollapsed(false)} style={{
- background:'rgba(78,205,196,0.2)', border:'1px solid rgba(78,205,196,0.5)',
- borderRadius:6, color:'#4ecdc4', cursor:'pointer', padding:'4px 6px',
- fontSize:14, lineHeight:1, fontWeight:700, width:'100%' }}>›
-
- ) : (
-
-
⚒
-
-
setCollapsed(true)} style={{
- background:'rgba(78,205,196,0.15)', border:'1px solid rgba(78,205,196,0.4)',
- borderRadius:6, color:'#4ecdc4', cursor:'pointer', padding:'4px 8px', flexShrink:0,
- fontSize:14, lineHeight:1, fontWeight:700 }}>‹
-
- )}
-
-
-
-
- {/* Apps Sektion mit Gruppenüberschriften */}
- setAppsOpen(v=>!v)} title={collapsed?'Apps':undefined} style={{
- width:'100%', display:'flex', alignItems:'center', gap:9,
- padding: collapsed ? '9px 0' : '9px 12px',
- justifyContent: collapsed ? 'center' : 'flex-start',
- borderRadius:7, border:'none', background:'transparent',
- cursor:'pointer', fontSize:12,
- fontFamily:"'Space Mono',monospace", textAlign:'left', marginBottom:1,
- borderLeft:'2px solid transparent',
- }}>
-
- {!collapsed && Apps }
- {!collapsed && }
-
- {appsOpen && getGroupedTools().map(([group, tools]) => (
-
- {!collapsed &&
{group.toUpperCase()}
}
- {tools.map(t =>
)}
-
- ))}
-
-
-
- {updateInfo?.hasUpdate && (
-
-
- Update
- {updateInfo.latestVersion}
-
- )}
-
- {!collapsed && <>
-
- {localStorage.getItem('dd_avatar')
- ?
- :
}
-
-
{user?.username}
-
-
Ausloggen >}
- {collapsed &&
}
-
-
- );
-}
-
-// ── Mobile Bottom Navigation ──────────────────────────────────────────────────
-function BottomNav({ active, setActive, user, onLogout, updateInfo, onUpdateClick, unreadMsgs=0 }) {
- const [menuOpen, setMenuOpen] = useState(false);
- const [appsOpen, setAppsOpen] = useState(false);
- const NAV_H = 56;
-
- const MI = ({Icon, icon, label, onClick, color='rgba(255,255,255,0.85)'}) => {
- const Ic = Icon;
- return (
-
-
- {Ic ? : {icon} }
-
- {label}
-
- );
- };
-
- const Sheet = ({open, onClose, children}) => {
- if (!open) return null;
- const isDesktop = window.innerWidth >= 768;
- if (isDesktop) return (
-
-
e.stopPropagation()}>
- {children}
-
-
- );
- return (
-
-
e.stopPropagation()}>
-
- {children}
-
-
-
- );
- };
-
- const mainNav = [
- { id:'dashboard', Icon:HomeIcon, label:'Home' },
- { id:'_apps', Icon:AppsIcon, label:'Apps' },
- { id:'admin', Icon:AdminIcon, label:'Einst.' },
- { id:'_mehr', Icon:MoreIcon, label:'Mehr' },
- ];
-
- return (
- <>
- {/* Apps Sheet mit Gruppenüberschriften */}
-
setAppsOpen(false)}>
-
- {getGroupedTools().map(([group, tools]) => (
-
-
{group.toUpperCase()}
- {tools.map(t => (
-
{ setActive(t.id); setAppsOpen(false); }} />
- ))}
-
- ))}
-
-
-
- {/* Mehr Sheet */}
-
setMenuOpen(false)}>
- {updateInfo?.hasUpdate && {setMenuOpen(false);onUpdateClick();}} color="#ffe66d"/>}
-
- {}} color="rgba(255,255,255,0.35)"/>
- {onLogout();setMenuOpen(false);}} color="#ff6b9d"/>
-
-
- {/* Bottom Bar */}
-
-
- {mainNav.map(item => {
- const isApps = item.id === '_apps';
- const isMehr = item.id === '_mehr';
- const isActive = isApps ? appsOpen : isMehr ? menuOpen : active === item.id;
- return (
- {
- if (isApps) { setAppsOpen(v=>!v); setMenuOpen(false); }
- else if (isMehr) { setMenuOpen(v=>!v); setAppsOpen(false); }
- else { setActive(item.id); setAppsOpen(false); setMenuOpen(false); }
- }}
- style={{ flex:1, display:'flex', flexDirection:'column', alignItems:'center',
- justifyContent:'center', gap:3, background:'transparent', border:'none',
- cursor:'pointer', position:'relative', minWidth:0, padding:'0 4px' }}>
- {isMehr && updateInfo?.hasUpdate && (
-
- )}
- {isApps && unreadMsgs>0 && (
-
- )}
- {(() => { const I = item.Icon; return I ? : null; })()}
- {item.label}
- {isActive && !isApps && !isMehr && (
-
- )}
-
- );
- })}
-
-
-
- >
- );
-}
-
-
-// ── Dashboard Widgets ─────────────────────────────────────────────────────────
-function QuickLinks({ toast, mobile }) {
- const { confirm, ConfirmDialog } = useConfirm();
- const [links, setLinks] = useState([]);
- const [editMode, setEdit] = useState(false);
- const [showForm, setForm] = useState(false);
- const [form, setF] = useState({ title:'', url:'', icon:'🔗' });
- const [editId, setEditId] = useState(null);
-
- useEffect(() => { api('/dashboard/links').then(setLinks).catch(() => {}); }, []);
-
- const getFavicon = url => {
- try { return `https://www.google.com/s2/favicons?sz=64&domain=${new URL(url).hostname}`; }
- catch { return null; }
- };
-
- const save = async () => {
- if (!form.title.trim() || !form.url.trim()) { toast('Titel und URL erforderlich', 'error'); return; }
- let url = form.url.trim();
- if (!/^https?:\/\//i.test(url)) url = 'https://' + url;
- // Auto favicon if user left default
- const icon = form.icon === '🔗' ? (getFavicon(url) || '🔗') : form.icon;
- try {
- if (editId) {
- const u = await api(`/dashboard/links/${editId}`, { method:'PUT', body:{...form, icon, url} });
- setLinks(p => p.map(l => l.id===editId ? u : l));
- } else {
- const n = await api('/dashboard/links', { body:{...form, icon, url} });
- setLinks(p => [...p, n]);
- }
- toast(editId ? 'Aktualisiert' : 'Gespeichert');
- setF({ title:'', url:'', icon:'🔗' }); setForm(false); setEditId(null);
- } catch(e) { toast(e.message, 'error'); }
- };
-
- const del = async id => {
- if (!await confirm('Link wirklich löschen?')) return;
- try { await api(`/dashboard/links/${id}`, { method:'DELETE' }); setLinks(p => p.filter(l => l.id!==id)); toast('Gelöscht'); }
- catch(e) { toast(e.message, 'error'); }
- };
-
- return (
-
-
-
-
- {/* Link-Grid */}
- {links.length === 0 && !editMode
- ?
Links können unter Einstellungen → Dashboard hinzugefügt werden.
- :
- {links.map(l => (
-
- ))}
-
- }
-
- {/* Formular */}
- {showForm && (
-
- {/* Icon-Picker */}
-
-
ICON
-
- {CONST_ICONS.map(ic => (
- setF(f => ({...f, icon:ic}))} style={{
- width:36, height:36, borderRadius:7, fontSize:18, cursor:'pointer',
- border:`1px solid ${form.icon===ic ? '#4ecdc4' : 'rgba(255,255,255,0.1)'}`,
- background: form.icon===ic ? 'rgba(78,205,196,0.15)' : 'rgba(255,255,255,0.04)',
- }}>{ic}
- ))}
-
-
-
-
- {editId ? '✓ Aktualisieren' : '✓ Speichern'}
- { setForm(false); setEditId(null); setF({ title:'', url:'', icon:'🔗' }); }} style={{ ...S.btn('#ff6b9d'), flex:1, textAlign:'center' }}>Abbrechen
-
-
- )}
-
- );
-}
-
-function TodoList({ toast }) {
- const [items, setItems] = useState([]);
- const [text, setText] = useState('');
- const [isOpen, setIsOpen] = useState(false);
- useEffect(() => { api('/dashboard/todos').then(setItems).catch(() => {}); }, []);
-
- const add = async () => {
- if (!text.trim()) return;
- try { const n = await api('/dashboard/todos', { body:{ text:text.trim() } }); setItems(p => [...p, n]); setText(''); }
- catch(e) { toast(e.message, 'error'); }
- };
- const toggle = async item => {
- try { const u = await api(`/dashboard/todos/${item.id}`, { method:'PUT', body:{ done:!item.done } }); setItems(p => p.map(i => i.id===item.id ? u : i)); }
- catch(e) { toast(e.message, 'error'); }
- };
- const del = async id => {
- try { await api(`/dashboard/todos/${id}`, { method:'DELETE' }); setItems(p => p.filter(i => i.id!==id)); }
- catch(e) { toast(e.message, 'error'); }
- };
-
- const open = items.filter(i => !i.done);
- const done = items.filter(i => i.done);
-
- return (
-
-
setIsOpen(v=>!v)} style={{width:'100%',background:'transparent',border:'none',
- display:'flex',justifyContent:'space-between',alignItems:'center',cursor:'pointer',padding:0}}>
- TO-DO{items.filter(i=>!i.done).length > 0
- ? ({items.filter(i=>!i.done).length})
- : null}
- ▶
-
- {isOpen && <>
- setText(e.target.value)}
- onKeyDown={e => e.key==='Enter' && add()}
- style={{ ...S.inp, flex:1, fontSize:16 }} placeholder="Neue Aufgabe…" />
- +
-
-
- {open.length === 0 && done.length === 0 && (
-
Noch keine Aufgaben.
- )}
- {open.map(item => (
-
- toggle(item)} style={{
- width:22, height:22, borderRadius:5, flexShrink:0, cursor:'pointer',
- border:'1.5px solid rgba(78,205,196,0.5)', background:'transparent',
- }} />
- {item.text}
- del(item.id)} style={{ background:'transparent', border:'none', color:'rgba(255,255,255,0.5)', cursor:'pointer', fontSize:18, padding:'0 2px', lineHeight:1 }}>✕
-
- ))}
- {done.length > 0 && (
- <>
-
ERLEDIGT
- {done.map(item => (
-
- toggle(item)} style={{
- width:22, height:22, borderRadius:5, flexShrink:0, cursor:'pointer',
- border:'1.5px solid rgba(78,205,196,0.4)', background:'rgba(78,205,196,0.15)',
- color:'#4ecdc4', fontSize:12, lineHeight:1,
- }}>✓
- {item.text}
- del(item.id)} style={{ background:'transparent', border:'none', color:'rgba(255,255,255,0.5)', cursor:'pointer', fontSize:18 }}>✕
-
- ))}
- >
- )}
-
>}
-
- );
-}
-
-function Notepad({ toast }) {
- const [content, setContent] = useState('');
- const [saved, setSaved] = useState(true);
- const [saveTime, setSaveTime] = useState(null);
- const [isOpen, setIsOpen] = useState(false);
- const tmr = useRef(null);
-
- useEffect(() => {
- api('/dashboard/note').then(d => { setContent(d.content || ''); setSaveTime(d.updated_at); }).catch(() => {});
- }, []);
-
- const change = val => {
- setContent(val); setSaved(false); clearTimeout(tmr.current);
- tmr.current = setTimeout(async () => {
- try { const r = await api('/dashboard/note', { method:'PUT', body:{ content:val } }); setSaved(true); setSaveTime(r.updated_at); }
- catch(e) { toast(e.message, 'error'); }
- }, 900);
- };
-
- return (
-
-
setIsOpen(v=>!v)} style={{width:'100%',background:'transparent',border:'none',
- display:'flex',alignItems:'center',gap:8,cursor:'pointer',padding:0}}>
- NOTIZEN
- {content.length > 0 && (
- ({content.length} Zeichen)
- )}
- ▶
-
- {isOpen &&
- {saved ? (saveTime ? `✓ ${new Date(saveTime).toLocaleTimeString('de-DE', { hour:'2-digit', minute:'2-digit' })}` : '✓') : '…'}
-
}
- {isOpen &&
- );
-}
-
-
-// ── Bestell-Statistik Widget ─────────────────────────────────────────────────
-function BestellStats() {
- const [stats, setStats] = useState(null);
- const [open, setOpen] = useState(false);
- const [loaded, setLoaded] = useState(false);
-
- const load = () => {
- if (loaded) return;
- api('/dashboard/stats').then(s => { setStats(s); setLoaded(true); }).catch(() => {});
- };
-
- const toggle = () => { setOpen(v => !v); if (!open) load(); };
-
- const Row = ({label, val, color='#fff'}) => (
-
- {label}
- {val}
-
- );
-
- return (
-
-
- 3D-DRUCK STATISTIK
- ▶
-
- {open && (
-
- {!stats ? (
-
Lädt…
- ) : (
- <>
-
-
BESTELLUNGEN
-
-
-
-
-
-
-
-
- >
- )}
-
- )}
-
- );
-}
-
-// ── Nachrichten Dashboard Widget ──────────────────────────────────────────────
-function NachrichtenWidget({ unreadMsgs, setActive }) {
- const [isOpen, setIsOpen] = useState(false);
- return (
-
-
setIsOpen(v=>!v)} style={{
- width:'100%', background:'transparent', border:'none', cursor:'pointer',
- display:'flex', alignItems:'center', padding:0, gap:8 }}>
-
- NACHRICHTEN
- {unreadMsgs > 0 && !isOpen && (
-
- {unreadMsgs} neu
-
- )}
- ▶
-
- {isOpen && (
-
- 0 ? '#4ecdc4' : 'rgba(255,255,255,0.35)',
- fontSize:12, fontFamily:'monospace' }}>
- {unreadMsgs > 0
- ? `${unreadMsgs} ungelesene Nachricht${unreadMsgs !== 1 ? 'en' : ''}`
- : 'Keine neuen Nachrichten'}
-
- setActive('nachrichten')} style={{ ...S.btn('#4ecdc4', true), flexShrink:0 }}>
- Öffnen →
-
-
- )}
-
- );
-}
-
-// ── Pushover Settings ─────────────────────────────────────────────────────────
-function PushoverSettings({ toast }) {
- const [form, setForm] = useState({ user_key:'', app_token:'' });
- const [busy, setBusy] = useState(false);
- const [hasSaved, setHasSaved] = useState(false);
-
- useEffect(() => {
- api('/tools/nachrichten/pushover')
- .then(d => {
- const v = { user_key: d.user_key||'', app_token: d.app_token||'' };
- setForm(v);
- setHasSaved(!!(v.user_key && v.app_token));
- }).catch(()=>{});
- }, []);
-
- const save = async () => {
- if (!form.user_key.trim() || !form.app_token.trim()) { toast('Beide Felder ausfüllen', 'error'); return; }
- setBusy(true);
- try { await api('/tools/nachrichten/pushover', { body: form }); toast('Gespeichert'); setHasSaved(true); }
- catch(e) { toast(e.message, 'error'); }
- setBusy(false);
- };
-
- const test = async () => {
- setBusy(true);
- try { await api('/tools/nachrichten/pushover/test', { method:'POST' }); toast('Testbenachrichtigung gesendet ✓'); }
- catch(e) { toast(e.message, 'error'); }
- setBusy(false);
- };
-
- const clear = async () => {
- try {
- await api('/tools/nachrichten/pushover', { method:'DELETE' });
- setForm({ user_key:'', app_token:'' });
- setHasSaved(false);
- toast('Pushover entfernt');
- } catch(e) { toast(e.message, 'error'); }
- };
-
- return (
-
-
- Push-Benachrichtigungen bei neuen Nachrichten via{' '}
- pushover.net .
- Du brauchst einen Account und eine eigene App (kostenlos).
-
-
setForm(p=>({...p,user_key:e.target.value}))} />
- setForm(p=>({...p,app_token:e.target.value}))} />
-
-
- {busy ? '…' : '✓ Speichern'}
-
- {hasSaved && (
- Test
- )}
- {hasSaved && (
- ✕
- )}
-
-
- );
-}
-
-function Dashboard({ toast, mobile, setActive, unreadMsgs=0 }) {
- const [now, setNow] = useState(new Date());
- useEffect(() => { const t = setInterval(() => setNow(new Date()), 30000); return () => clearInterval(t); }, []);
- return (
-
-
-
-
Dashboard
-
- {now.toLocaleDateString('de-DE', { weekday:'long', day:'numeric', month:'long' })}
- {' · '}
- {now.toLocaleTimeString('de-DE', { hour:'2-digit', minute:'2-digit' })}
-
-
-
window.location.reload()}
- title="Seite neu laden"
- style={{ background:'transparent', border:'1px solid rgba(255,255,255,0.1)', borderRadius:8,
- color:'rgba(255,255,255,0.4)', cursor:'pointer', padding:'6px 10px', fontSize:14,
- fontFamily:'monospace', marginTop:2, flexShrink:0 }}>↺
-
-
-
-
-
-
-
-
- {typeof __BUILD_TIME__ !== 'undefined' && __BUILD_TIME__
- ? `Letzte Versionierung: ${__BUILD_TIME__}` : null}
-
-
- );
-}
-
-
-// ── Admin Panel ───────────────────────────────────────────────────────────────
-// Sec muss außerhalb von AdminPanel definiert sein damit Inputs den Fokus behalten
-const Sec = ({title, children}) => (
-
-);
-
-// ── Account-Löschung ─────────────────────────────────────────────────────────
-function DeleteAccountSection({ toast, user }) {
- const [pw, setPw] = useState('');
- const [busy, setBusy] = useState(false);
- const { confirm, ConfirmDialog } = useConfirm();
-
- const doDelete = async () => {
- if (!pw) { toast('Passwort eingeben', 'error'); return; }
- if (!await confirm(`Account "${user.username}" und alle Daten wirklich löschen? Das kann nicht rückgängig gemacht werden.`)) return;
- setBusy(true);
- try {
- await api('/auth/delete-account', { method:'DELETE', body:{ password:pw } });
- localStorage.removeItem('sk_token');
- window.location.reload();
- } catch(e) { toast(e.message, 'error'); } finally { setBusy(false); }
- };
-
- return (
-
-
- setPw(e.target.value)}/>
-
- {busy ? '…' : '✕ Account unwiderruflich löschen'}
-
-
- );
-}
-
-// ── QuickLinks Einstellungen ─────────────────────────────────────────────────
-function QuickLinksSettings({ toast }) {
- const [links, setLinks] = useState([]);
- const [showForm, setForm] = useState(false);
- const [form, setF] = useState({ title:'', url:'', icon:'🔗' });
- const [editId, setEditId] = useState(null);
- const { confirm, ConfirmDialog } = useConfirm();
-
- useEffect(() => { api('/dashboard/links').then(setLinks).catch(() => {}); }, []);
-
- const getFavicon = url => {
- try { return `https://www.google.com/s2/favicons?sz=64&domain=${new URL(url).hostname}`; }
- catch { return null; }
- };
-
- const save = async () => {
- if (!form.title.trim() || !form.url.trim()) { toast('Titel und URL erforderlich', 'error'); return; }
- let url = form.url.trim();
- if (!/^https?:\/\//i.test(url)) url = 'https://' + url;
- const icon = form.icon === '🔗' ? (getFavicon(url) || '🔗') : form.icon;
- try {
- if (editId) {
- const u = await api(`/dashboard/links/${editId}`, { method:'PUT', body:{...form, icon, url} });
- setLinks(p => p.map(l => l.id===editId ? u : l));
- } else {
- const n = await api('/dashboard/links', { body:{...form, icon, url} });
- setLinks(p => [...p, n]);
- }
- toast(editId ? 'Aktualisiert' : 'Gespeichert');
- setF({ title:'', url:'', icon:'🔗' }); setForm(false); setEditId(null);
- } catch(e) { toast(e.message, 'error'); }
- };
-
- const del = async id => {
- if (!await confirm('Link wirklich löschen?')) return;
- try { await api(`/dashboard/links/${id}`, { method:'DELETE' }); setLinks(p => p.filter(l => l.id!==id)); toast('Gelöscht'); }
- catch(e) { toast(e.message, 'error'); }
- };
-
- return (
-
-
- {/* Link-Liste */}
- {links.length > 0 && (
-
- {links.map(l => (
-
-
- {l.icon && l.icon.startsWith('http')
- ? e.target.style.display='none'}/>
- : l.icon}
-
-
-
- { setF({ title:l.title, url:l.url, icon:l.icon }); setEditId(l.id); setForm(true); }}
- style={S.btn('#4ecdc4', true)}>✎
- del(l.id)} style={S.btn('#ff6b9d', true)}>✕
-
-
- ))}
-
- )}
-
- {/* Formular */}
- {showForm ? (
-
-
-
ICON
-
- {['🔗','🌐','📊','📁','🛠','📧','🗓','💾','🖥','📡','🔒','📖','🎯','⚡','🏠','🔧','📱','🎮','🚀','💡'].map(ic => (
- setF(f => ({...f, icon:ic}))} style={{
- width:34, height:34, borderRadius:7, fontSize:17, cursor:'pointer',
- border:`1px solid ${form.icon===ic ? '#4ecdc4' : 'rgba(255,255,255,0.1)'}`,
- background: form.icon===ic ? 'rgba(78,205,196,0.15)' : 'rgba(255,255,255,0.04)',
- }}>{ic}
- ))}
-
-
-
setF(f => ({...f, title:e.target.value}))} placeholder="Mein Link"/>
- setF(f => ({...f, url:e.target.value}))} placeholder="https://…" autoCapitalize="none"/>
-
- {editId ? '✓ Aktualisieren' : '✓ Speichern'}
- { setForm(false); setEditId(null); setF({ title:'', url:'', icon:'🔗' }); }}
- style={{ ...S.btn('#ff6b9d'), flex:1, textAlign:'center', padding:'10px 0' }}>Abbrechen
-
-
- ) : (
-
{ setForm(true); setEditId(null); setF({ title:'', url:'', icon:'🔗' }); }}
- style={{ ...S.btn('#4ecdc4'), width:'100%', textAlign:'center', padding:'10px 0' }}>
- + Link hinzufügen
-
- )}
-
- );
-}
-
-// ── Benutzerverwaltung ────────────────────────────────────────────────────────
-function UserManagement({ toast }) {
- const [users, setUsers] = useState([]);
- const [loading, setLoading] = useState(true);
- const [form, setForm] = useState({ username:'', password:'', role:'user' });
- const [resetId, setResetId] = useState(null);
- const [resetPw, setResetPw] = useState('');
- const [delId, setDelId] = useState(null);
- const { confirm, ConfirmDialog } = useConfirm();
-
- const load = () => {
- setLoading(true);
- api('/admin/users').then(setUsers).catch(e=>toast(e.message,'error')).finally(()=>setLoading(false));
- };
- useEffect(()=>{ load(); },[]);
-
- const create = async () => {
- if (!form.username.trim()||!form.password) { toast('Alle Felder ausfüllen','error'); return; }
- try {
- await api('/admin/users',{body:form});
- toast(`Benutzer "${form.username}" angelegt`);
- setForm({username:'',password:'',role:'user'}); load();
- } catch(e) { toast(e.message,'error'); }
- };
-
- const del = async id => {
- if (!await confirm('Benutzer und alle seine Daten wirklich löschen?')) return;
- try { await api(`/admin/users/${id}`,{method:'DELETE'}); toast('Gelöscht'); load(); }
- catch(e) { toast(e.message,'error'); }
- };
-
- const resetPassword = async () => {
- if (!resetPw||resetPw.length<8) { toast('Mind. 8 Zeichen','error'); return; }
- try {
- await api(`/admin/users/${resetId}/reset-password`,{method:'PUT',body:{newPassword:resetPw}});
- toast('Passwort zurückgesetzt'); setResetId(null); setResetPw('');
- } catch(e) { toast(e.message,'error'); }
- };
-
- return (
-
-
-
- {/* Neuen Benutzer anlegen */}
-
- setForm(f=>({...f,username:e.target.value}))} autoCapitalize="none"/>
- setForm(f=>({...f,password:e.target.value}))}/>
-
- ROLLE
- setForm(f=>({...f,role:e.target.value}))} style={{...S.inp,fontSize:14}}>
- Benutzer
- Administrator
-
-
-
- + Benutzer anlegen
-
-
-
- {/* Passwort-Reset Modal */}
- {resetId && (
-
-
-
- Passwort zurücksetzen
-
-
setResetPw(e.target.value)}/>
-
- ✓ Setzen
- {setResetId(null);setResetPw('');}} style={{...S.btn('#ff6b9d'),flex:1,textAlign:'center',padding:'10px 0'}}>Abbrechen
-
-
-
- )}
-
- {/* Benutzerliste */}
-
- {loading && Lädt…
}
- {users.map(u => (
-
-
-
- {u.username}
- {u.role}
-
-
- {setResetId(u.id);setResetPw('');}}
- style={{...S.btn('#ffe66d',true),fontSize:10}}>🔑 PW
- del(u.id)} style={{...S.btn('#ff6b9d',true)}}>
-
-
-
-
- {/* Statistik */}
-
- {[
- ['◈ Archiv', u.archiv],
- ['📦 Bestellungen', u.bestellung],
- ['✓ Todos', u.todos],
- ['🔗 Links', u.links],
- ['📁 Dateien', u.files],
- ['📅 iCals', u.icals],
- [`✎ ${u.noteLen} Zeichen`, null],
- ].map(([label, val]) => (
-
- {label}{val !== null ? `: ${val}` : ''}
-
- ))}
-
-
- ))}
-
-
- );
-}
-
-// ── Sicherheit / Key-Management ───────────────────────────────────────────────
-function SecuritySettings({ toast }) {
- const [kp, setKp] = useState(null);
- const [fingerprint, setFp] = useState('');
- const [expPw, setExpPw] = useState('');
- const [expPw2, setExpPw2] = useState('');
- const [showExport, setShowExport] = useState(false);
- const [impFile, setImpFile] = useState(null);
- const [impPw, setImpPw] = useState('');
- const [showImport, setShowImport] = useState(false);
- const [busy, setBusy] = useState(false);
-
- useEffect(() => {
- getOrCreateKeyPair()
- .then(async k => {
- setKp(k);
- setFp(await getFingerprint(k));
- })
- .catch(() => {});
- }, []);
-
- const doExport = async () => {
- if (!kp) return;
- if (expPw.length < 8) { toast('Passwort mind. 8 Zeichen', 'error'); return; }
- if (expPw !== expPw2) { toast('Passwörter stimmen nicht überein', 'error'); return; }
- setBusy(true);
- try {
- const json = await exportEncrypted(kp, expPw);
- const a = Object.assign(document.createElement('a'), {
- href: URL.createObjectURL(new Blob([json], { type:'application/json' })),
- download: 'dickendock-key.json',
- });
- a.click(); URL.revokeObjectURL(a.href);
- setExpPw(''); setExpPw2(''); setShowExport(false);
- toast('Schlüssel exportiert ✓');
- } catch { toast('Export fehlgeschlagen', 'error'); }
- setBusy(false);
- };
-
- const doImport = async () => {
- if (!impFile || !impPw) { toast('Datei und Passwort erforderlich', 'error'); return; }
- setBusy(true);
- try {
- const text = await impFile.text();
- const newKp = await importEncrypted(text, impPw);
- const jwk = await getPublicKeyJwk(newKp);
- await api('/tools/nachrichten/keys', { body: { public_key: JSON.stringify(jwk) } });
- setKp(newKp);
- setFp(await getFingerprint(newKp));
- setImpFile(null); setImpPw(''); setShowImport(false);
- toast('Schlüssel importiert ✓');
- } catch(e) { toast(e.message, 'error'); }
- setBusy(false);
- };
-
- const doReset = async () => {
- if (!window.confirm(
- 'Neues Schlüsselpaar erzeugen?\n\n' +
- 'Alle bisherigen Nachrichten werden vom Server gelöscht und können nicht mehr gelesen werden. ' +
- 'Geräte mit dem alten Schlüssel können keine neuen Nachrichten mehr lesen.'
- )) return;
- setBusy(true);
- try {
- const newKp = await generateNewKeyPair();
- const jwk = await getPublicKeyJwk(newKp);
- await api('/tools/nachrichten/keys', { body: { public_key: JSON.stringify(jwk) } });
- await api('/tools/nachrichten/my-messages', { method:'DELETE' });
- toast('Neues Schlüsselpaar aktiv – App wird neu geladen…');
- setTimeout(() => window.location.reload(), 1500);
- } catch(e) { toast(e.message, 'error'); setBusy(false); }
- };
-
- const FpChar = ({ c }) => (
-
{c}
- );
-
- return (
-
- {/* Fingerprint */}
-
-
- Das ist der Fingerprint deines eigenen Public Keys.
- Dein Gesprächspartner sieht im Chat-Header deinen Fingerprint – er soll ihn mit diesem hier vergleichen.
- Stimmen beide überein, ist die Verbindung sicher.
-
- {fingerprint ? (
-
- {fingerprint.split('').map((c, i) => )}
-
- ) : (
- Wird geladen…
- )}
-
- SHA-256(PublicKey) · X25519
-
-
-
- {/* Export */}
-
-
- Exportiere deinen privaten Schlüssel verschlüsselt als Datei um ihn auf einem anderen Gerät zu nutzen.
-
- {!showExport ? (
- setShowExport(true)} disabled={!kp}
- style={{ ...S.btn('#4ecdc4'), width:'100%', textAlign:'center', padding:'10px 0' }}>
- ↓ Schlüssel exportieren
-
- ) : (
-
-
setExpPw(e.target.value)}/>
- setExpPw2(e.target.value)}/>
-
-
- {busy ? '…' : '↓ Herunterladen'}
-
- { setShowExport(false); setExpPw(''); setExpPw2(''); }}
- style={{ ...S.btn('#ff6b9d', true), padding:'0 14px' }}>Abbrechen
-
-
- )}
-
-
- {/* Import */}
-
-
- Importiere eine zuvor exportierte Schlüsseldatei. Der aktuelle Schlüssel wird ersetzt.
-
- {!showImport ? (
- setShowImport(true)}
- style={{ ...S.btn('#ffe66d'), width:'100%', textAlign:'center', padding:'10px 0' }}>
- ↑ Schlüssel importieren
-
- ) : (
-
-
- SCHLÜSSELDATEI (.json)
- setImpFile(e.target.files?.[0] || null)}
- style={{ ...S.inp, fontSize:13, padding:'9px 10px', cursor:'pointer' }}/>
-
-
setImpPw(e.target.value)}/>
-
-
- {busy ? '…' : '✓ Importieren'}
-
- { setShowImport(false); setImpFile(null); setImpPw(''); }}
- style={{ ...S.btn('#ff6b9d', true), padding:'0 14px' }}>Abbrechen
-
-
- )}
-
-
- {/* Reset – Danger Zone */}
-
-
- ⚠ Gefahrenzone: Erzeugt ein neues Schlüsselpaar und löscht alle deine Nachrichten vom Server.
- Alte Nachrichten sind danach nicht mehr lesbar. Geräte mit dem alten Schlüssel werden ausgesperrt.
-
-
- {busy ? '…' : '✕ Neues Schlüsselpaar erzeugen'}
-
-
-
- );
-}
-
-// Field außerhalb definiert → Tastatur bleibt beim Tippen erhalten
-const Field = ({ label, type='text', value, onChange, placeholder='', autoCapitalize='off' }) => (
-
- {label}
-
-
-);
-
-function AdminPanel({ toast, mobile, user }) {
- const [section, setSection] = useState('profil');
- const [pw, setPw] = useState({ current:'', newPw:'', confirm:'' });
- const [avatar, setAvatar] = useState(localStorage.getItem('dd_avatar') || null);
- const [un, setUn] = useState({ newUsername:'', unPw:'' });
- const [file, setFile] = useState(null);
- const [busy, setBusy] = useState({ pw:false, un:false, restore:false });
- const set = (key, val) => setBusy(b => ({...b, [key]:val}));
-
- const changePw = async () => {
- if (pw.newPw !== pw.confirm) { toast('Passwörter stimmen nicht überein', 'error'); return; }
- if (pw.newPw.length < 8) { toast('Mindestens 8 Zeichen', 'error'); return; }
- set('pw', true);
- try { await api('/auth/change-password', { body:{ currentPassword:pw.current, newPassword:pw.newPw } }); toast('Passwort geändert'); setPw({ current:'', newPw:'', confirm:'' }); }
- catch(e) { toast(e.message, 'error'); } finally { set('pw', false); }
- };
-
- const changeUn = async () => {
- if (!un.newUsername.trim()) { toast('Benutzername eingeben', 'error'); return; }
- set('un', true);
- try {
- await api('/auth/change-username', { body:{ newUsername:un.newUsername.trim(), password:un.unPw } });
- toast('Benutzername geändert – bitte neu einloggen');
- setUn({ newUsername:'', unPw:'' });
- setTimeout(() => { localStorage.removeItem('sk_token'); window.location.reload(); }, 1800);
- } catch(e) { toast(e.message, 'error'); } finally { set('un', false); }
- };
-
- const backup = async () => {
- try {
- const blob = await api('/admin/backup');
- const a = Object.assign(document.createElement('a'), {
- href: URL.createObjectURL(blob),
- download: `dickendock-backup-${new Date().toISOString().split('T')[0]}.db`
- });
- a.click(); URL.revokeObjectURL(a.href); toast('Backup heruntergeladen');
- } catch(e) { toast(e.message, 'error'); }
- };
-
- const restore = async () => {
- if (!file) { toast('Datei auswählen', 'error'); return; }
- if (!window.confirm('⚠️ Aktuelle Datenbank wird ersetzt. Fortfahren?')) return;
- set('restore', true);
- try {
- const f = new FormData(); f.append('database', file);
- await api('/admin/restore', { method:'POST', body:f, isFile:true });
- toast('Wiederhergestellt'); setFile(null);
- } catch(e) { toast(e.message, 'error'); } finally { set('restore', false); }
- };
-
- return (
-
-
-
Einstellungen
- window.location.reload()} title="Seite neu laden"
- style={{ background:'transparent', border:'1px solid rgba(255,255,255,0.1)', borderRadius:8,
- color:'rgba(255,255,255,0.4)', cursor:'pointer', padding:'6px 10px', fontSize:14, fontFamily:'monospace' }}>↺
-
-
-
- {[['profil','Profil'],['sicherheit','Sicherheit'],['dashboard','Dashboard'], ...(user?.role==='admin'?[['benutzer','Benutzer'],['backup','Backup']]:[])]
- .map(([k,l]) => (
- setSection(k)} style={{
- padding:'6px 16px', borderRadius:20, fontFamily:'monospace', fontSize:12, cursor:'pointer',
- border:`1px solid ${section===k?'rgba(78,205,196,0.5)':'rgba(255,255,255,0.15)'}`,
- background: section===k?'rgba(78,205,196,0.12)':'transparent',
- color: section===k?'#4ecdc4':'rgba(255,255,255,0.55)',
- }}>{l}
- ))}
-
-
- {section === 'profil' && (
-
-
-
-
- {avatar
- ?
- :
}
-
-
- PROFILBILD
- {
- const file = e.target.files?.[0]; if(!file) return;
- const reader = new FileReader();
- reader.onload = ev => {
- const canvas = document.createElement('canvas');
- const img = new Image(); img.onload = () => {
- const size = 120;
- canvas.width = size; canvas.height = size;
- const ctx = canvas.getContext('2d');
- ctx.beginPath(); ctx.arc(size/2,size/2,size/2,0,Math.PI*2);
- ctx.clip();
- const min = Math.min(img.width,img.height);
- ctx.drawImage(img,(img.width-min)/2,(img.height-min)/2,min,min,0,0,size,size);
- const data = canvas.toDataURL('image/jpeg',0.85);
- setAvatar(data); localStorage.setItem('dd_avatar',data); toast('Avatar gespeichert');
- }; img.src = ev.target.result;
- }; reader.readAsDataURL(file);
- }} style={{...S.inp,fontSize:13,padding:'7px 10px',cursor:'pointer'}}/>
-
-
- {avatar && {setAvatar(null);localStorage.removeItem('dd_avatar');toast('Avatar entfernt');}}
- style={{...S.btn('#ff6b9d',true)}}>✕ Entfernen }
-
- {user?.role==='admin' &&
- setUn(p=>({...p,newUsername:e.target.value}))} />
- setUn(p=>({...p,unPw:e.target.value}))} />
-
- {busy.un ? '…' : '✓ Benutzername ändern'}
-
- }
-
- setPw(p=>({...p,current:e.target.value}))} />
- setPw(p=>({...p,newPw:e.target.value}))} />
- setPw(p=>({...p,confirm:e.target.value}))} />
-
- {busy.pw ? '…' : '✓ Passwort ändern'}
-
-
-
-
- Löscht deinen Account und alle deine Daten unwiderruflich.
-
-
-
-
-
-
-
- )}
-
- {section === 'sicherheit' && (
-
- )}
-
- {section === 'benutzer' && user?.role==='admin' && (
-
- )}
-
- {section === 'dashboard' && (
-
-
-
-
-
-
-
- {user?.role==='admin' &&
-
- }
-
- )}
-
- {section === 'backup' && (
-
-
-
- Vollständige SQLite-Datenbank sichern oder wiederherstellen.
-
-
- ↓ Backup herunterladen
-
- SICHERUNG EINSPIELEN (.db)
-
- setFile(e.target.files[0])}
- style={{ ...S.inp, flex:1, padding:'9px 10px', cursor:'pointer', fontSize:13 }} />
-
- {busy.restore ? '…' : '↑'}
-
-
- {file && {file.name}
}
-
-
- )}
-
- );
-}
-
-
-// ── App Shell ─────────────────────────────────────────────────────────────────
-export default function App() {
- const mobile = useIsMobile();
- const [splash, setSplash] = useState(() => {
- // Only show splash in standalone PWA mode, and only once per session
- return window.matchMedia('(display-mode: standalone)').matches && !sessionStorage.getItem('splash_shown');
- });
- const [user,setUser]=useState(null);
- const [active,setActive]=useState('dashboard');
- const [toastMsg,setToastMsg]=useState({msg:'',type:'success'});
- const [updateInfo,setUpdateInfo]=useState(null);
- const [showUpd,setShowUpd]=useState(false);
- const [unreadMsgs,setUnreadMsgs]=useState(0);
-
- const [authChecked, setAuthChecked] = useState(false);
- useEffect(()=>{
- const token=localStorage.getItem('sk_token');
- if(token){
- try{const p=JSON.parse(atob(token.split('.')[1]));
- if(p.exp*1000>Date.now())setUser({id:p.id,username:p.username,role:p.role});
- else localStorage.removeItem('sk_token');
- }catch{localStorage.removeItem('sk_token');}
- }
- setAuthChecked(true);
- },[]);
-
- useEffect(()=>{
- if(!user)return;
- const check=()=>api('/system/update-check').then(setUpdateInfo).catch(()=>{});
- check();
- const t=setInterval(check,5*60*1000);
- return()=>clearInterval(t);
- },[user]);
-
- // Schlüssel sofort nach Login initialisieren und Public Key registrieren
- useEffect(()=>{
- if(!user)return;
- getOrCreateKeyPair().then(async kp=>{
- try{
- const jwk=await getPublicKeyJwk(kp);
- await api('/tools/nachrichten/keys',{body:{public_key:JSON.stringify(jwk)}});
- }catch{}
- }).catch(()=>{});
- },[user]);
-
- useEffect(()=>{
- if(!user)return;
- const poll=()=>api('/tools/nachrichten/unread').then(d=>setUnreadMsgs(d.count||0)).catch(()=>{});
- poll();
- const t=setInterval(poll,30*1000);
- return()=>clearInterval(t);
- },[user]);
-
- const toast=useCallback((msg,type='success')=>{
- setToastMsg({msg,type});
- setTimeout(()=>setToastMsg({msg:'',type:'success'}),3500);
- },[]);
-
- if(splash) return
{ setSplash(false); sessionStorage.setItem('splash_shown','1'); }}/>;
- if(!authChecked) return ;
- if(!user) return
setUser(u)}/>;
-
- const activeTool = TOOLS.find(t=>t.id===active);
-
- // Mobile padding bottom für fixed nav
- const mainStyle = { flex:1, overflowY:'auto', ...(mobile ? { paddingBottom:'calc(56px + env(safe-area-inset-bottom, 0px))' } : {}) };
-
- return(
- <>
-
-
- {!mobile && (
-
{localStorage.removeItem('sk_token');setUser(null);}}
- updateInfo={updateInfo} onUpdateClick={()=>setShowUpd(true)}
- unreadMsgs={unreadMsgs}/>
- )}
-
- {active==='dashboard' && }
- {active==='admin' && }
- {activeTool && }
-
-
- {mobile && (
- {localStorage.removeItem('sk_token');setUser(null);}}
- updateInfo={updateInfo} onUpdateClick={()=>setShowUpd(true)}
- unreadMsgs={unreadMsgs}/>
- )}
-
- {showUpd&&updateInfo&&setShowUpd(false)}/>}
- >
- );
-}