diff --git a/frontend/App.jsx b/frontend/App.jsx
deleted file mode 100644
index 98b6a9b..0000000
--- a/frontend/App.jsx
+++ /dev/null
@@ -1,919 +0,0 @@
-import { useState, useEffect, useRef, useCallback } from 'react';
-import { api, S } from './lib.js';
-import { TOOLS, getGroupedTools } from './toolRegistry.js';
-import { useConfirm } from './confirm.jsx';
-import { HomeIcon, AppsIcon, AdminIcon, MoreIcon, UserIcon, LogOutIcon, ChevronIcon, UpdateIcon } from './icons.jsx';
-
-const ICONS = ['๐','๐','๐','๐','๐ ','๐ง','๐','๐พ','๐ฅ','๐ก','๐','๐','๐ฏ','โก','๐ ','๐ง','๐ฑ','๐ฎ','๐','๐ก'];
-
-// โโ 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 (
- e.target===e.currentTarget&&onClose()}>
-
-
-
- {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); 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])=>(
-
-
- 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}
}
-
-
-
- );
-}
-
-// โโ 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 }) {
- 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 (
-
- );
- };
-
- return (
-
- );
-}
-
-// โโ Mobile Bottom Navigation โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
-function BottomNav({ active, setActive, user, onLogout, updateInfo, onUpdateClick }) {
- 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 (
-
- );
- };
-
- const Sheet = ({open, onClose, children}) => !open ? null : (
-
-
e.stopPropagation()}>
-
- {children}
-
-
-
- );
-
- const mainNav = [
- { id:'dashboard', Icon:HomeIcon, label:'Home' },
- { id:'_apps', Icon:AppsIcon, label:'Apps' },
- ...(user?.role==='admin' ? [{ id:'admin', Icon:AdminIcon, label:'Einstellungen' }] : []),
- { 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 */}
-
- >
- );
-}
-
-
-// โโ 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 (
-
-
-
-
SCHNELLZUGRIFF
-
- {editMode && }
-
-
-
-
- {/* Link-Grid */}
- {links.length === 0 && !editMode
- ?
Tippe auf "โ" um Links hinzuzufรผgen.
- :
- {links.map(l => (
-
- {editMode && (
-
-
-
-
- )}
-
e.preventDefault() : undefined}
- style={{ display:'flex', flexDirection:'column', alignItems:'center', gap:4,
- padding:'10px 14px', background:'rgba(255,255,255,0.04)',
- border:'1px solid rgba(255,255,255,0.08)', borderRadius:10,
- textDecoration:'none', minWidth:60, cursor:editMode ? 'default' : 'pointer' }}>
- {l.icon && l.icon.startsWith('http')
- ?
e.target.style.display='none'}/>
- : {l.icon}}
- {l.title}
-
-
- ))}
-
- }
-
- {/* Formular */}
- {showForm && (
-
- {/* Icon-Picker */}
-
-
ICON
-
- {ICONS.map(ic => (
-
- ))}
-
-
-
-
-
-
-
-
- )}
-
- );
-}
-
-function TodoList({ toast }) {
- const [items, setItems] = useState([]);
- const [text, setText] = useState('');
- 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 (
-
-
TO-DO
-
- 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 => (
-
-
- ))}
- {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 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 (
-
-
-
NOTIZEN
-
- {saved ? (saveTime ? `โ ${new Date(saveTime).toLocaleTimeString('de-DE', { hour:'2-digit', minute:'2-digit' })}` : 'โ') : 'โฆ'}
-
-
-
- );
-}
-
-
-// โโ 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
-
-
-
-
-
-
-
-
- >
- )}
-
- )}
-
- );
-}
-
-function Dashboard({ toast, mobile }) {
- return (
-
-
-
Dashboard
-
- {new Date().toLocaleDateString('de-DE', { weekday:'long', day:'numeric', month:'long' })}
-
-
-
-
-
-
-
- );
-}
-
-
-// โโ Admin Panel โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
-// Sec muss auรerhalb von AdminPanel definiert sein damit Inputs den Fokus behalten
-const Sec = ({title, children}) => (
-
-);
-
-// Field auรerhalb definiert โ Tastatur bleibt beim Tippen erhalten
-const Field = ({ label, type='text', value, onChange, placeholder='', autoCapitalize='off' }) => (
-
-
-
-
-);
-
-function AdminPanel({ toast, mobile }) {
- 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
-
-
- {[['profil','Profil'],['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
- ?

- :
}
-
-
-
- {
- 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}
-
-
- 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'}
-
-
-
- )}
-
- {section === 'backup' && (
-
-
-
- Vollstรคndige SQLite-Datenbank sichern oder wiederherstellen.
-
-
- โ Backup herunterladen
-
-
-
- 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(() => window.matchMedia('(display-mode: standalone)').matches);
- 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);
-
- useEffect(()=>{
- const token=localStorage.getItem('sk_token');
- if(!token)return;
- 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');}
- },[]);
-
- 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]);
-
- const toast=useCallback((msg,type='success')=>{
- setToastMsg({msg,type});
- setTimeout(()=>setToastMsg({msg:'',type:'success'}),3500);
- },[]);
-
- if(splash) return setSplash(false)}/>;
- 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)}/>
- )}
-
- {active==='dashboard' && }
- {active==='admin' && user?.role==='admin' && }
- {activeTool && }
-
-
- {mobile && (
- {localStorage.removeItem('sk_token');setUser(null);}}
- updateInfo={updateInfo} onUpdateClick={()=>setShowUpd(true)}/>
- )}
-
- {showUpd&&updateInfo&&setShowUpd(false)}/>}
- >
- );
-}