Files
dickendock/frontend/src/tools/devtools.jsx

773 lines
39 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useState } from 'react';
import { S } from '../lib.js';
const MONTHS = ['Januar','Februar','März','April','Mai','Juni','Juli','August','September','Oktober','November','Dezember'];
const WEEKDAYS = ['Sonntag','Montag','Dienstag','Mittwoch','Donnerstag','Freitag','Samstag'];
function parseField(val) {
val = String(val).trim();
if (val==='*'||val==='?') return {type:'all'};
if (val.startsWith('*/')) { const n=parseInt(val.slice(2)); return {type:'step',step:n}; }
if (val.includes('/')&&!val.startsWith('*/')) { const [s,st]=val.split('/'); return {type:'stepFrom',from:parseInt(s),step:parseInt(st)}; }
if (val.includes(',')) return {type:'list',vals:val.split(',').map(v=>parseInt(v))};
if (val.includes('-')) { const [a,b]=val.split('-').map(Number); return {type:'range',from:a,to:b}; }
const n=parseInt(val); if (!isNaN(n)) return {type:'single',val:n};
return {type:'raw',val};
}
function describeField(f, names) {
switch(f.type) {
case 'all': return null;
case 'step': return `alle ${f.step}`;
case 'stepFrom': return `ab ${f.from} alle ${f.step}`;
case 'list': return f.vals.map(v=>names?.[v]||v).join(', ');
case 'range': return `${names?.[f.from]||f.from}${names?.[f.to]||f.to}`;
case 'single': return names?.[f.val] || String(f.val);
default: return f.val;
}
}
function explainCron(expr) {
const parts = expr.trim().split(/\s+/);
const specials = {'@yearly':'0 0 1 1 *','@annually':'0 0 1 1 *','@monthly':'0 0 1 * *',
'@weekly':'0 0 * * 0','@daily':'0 0 * * *','@midnight':'0 0 * * *','@hourly':'0 * * * *'};
if (parts.length===1) {
if (parts[0]==='@reboot') return {ok:true,human:'Beim Systemstart',fields:null};
if (specials[parts[0]]) return explainCron(specials[parts[0]]);
}
if (parts.length!==5) return {ok:false,error:'Ein Crontab besteht aus 5 Feldern: Minute Stunde Tag Monat Wochentag'};
const [minP,hrP,domP,monP,dowP] = parts;
const min=parseField(minP), hr=parseField(hrP), dom=parseField(domP), mon=parseField(monP), dow=parseField(dowP);
const out = [];
if (hr.type==='all'&&min.type==='all') out.push('Jede Minute');
else if (hr.type==='all'&&min.type==='step') out.push(`Alle ${min.step} Minuten`);
else if (hr.type==='all') out.push(`Jede Stunde um :${String(describeField(min)).padStart(2,'0')}`);
else if (min.type==='all') out.push(`Jede Minute der Stunde(n) ${describeField(hr)}`);
else { const m=describeField(min), h=describeField(hr); out.push(`${String(h).padStart(2,'0')}:${String(m).padStart(2,'0')} Uhr`); }
if (dow.type!=='all') out.push(`jeden ${describeField(dow,WEEKDAYS)}`);
if (dom.type!=='all') { const d=describeField(dom); out.push(`am ${d}. des Monats`); }
if (mon.type!=='all') out.push(`im ${describeField(mon,MONTHS)}`);
return {ok:true,human:out.join(', '),fields:{min,hr,dom,mon,dow},raw:parts};
}
function nextRuns(expr, count=5) {
try {
const parts = expr.trim().split(/\s+/);
if (parts.length!==5) return [];
const [minP,hrP,domP,monP,dowP] = parts;
const fMin=parseField(minP), fHr=parseField(hrP), fDom=parseField(domP), fMon=parseField(monP), fDow=parseField(dowP);
const match = (f, v) => {
switch(f.type){
case 'all': return true;
case 'single': return v===f.val||(f.val===7&&v===0);
case 'range': return v>=f.from&&v<=f.to;
case 'step': return v%f.step===0;
case 'stepFrom': return v>=f.from&&(v-f.from)%f.step===0;
case 'list': return f.vals.includes(v);
default: return false;
}
};
const results=[], d=new Date();
d.setSeconds(0); d.setMilliseconds(0); d.setMinutes(d.getMinutes()+1);
for (let i=0;i<50000&&results.length<count;i++){
if (match(fMon,d.getMonth()+1)&&match(fDom,d.getDate())&&
(fDow.type==='all'||match(fDow,d.getDay()))&&
match(fHr,d.getHours())&&match(fMin,d.getMinutes())) results.push(new Date(d));
d.setMinutes(d.getMinutes()+1);
}
return results;
} catch { return []; }
}
// ── Erklärer ──────────────────────────────────────────────────────────────────
function CronExplainer() {
const [input, setInput] = useState('');
const expr = input.trim();
const result = expr ? explainCron(expr) : null;
const runs = result?.ok ? nextRuns(expr) : [];
const fmtDt = d => d.toLocaleString('de-DE',{weekday:'short',day:'2-digit',month:'2-digit',year:'numeric',hour:'2-digit',minute:'2-digit'});
const EXAMPLES = [['0 8 * * 1','Mo 8:00'],['*/15 * * * *','alle 15 Min'],['0 0 1 * *','1. jeden Monat'],['30 18 * * 1-5','MoFr 18:30'],['0 9,17 * * *','9 & 17 Uhr'],['0 0 * * 0','So 0:00']];
return (
<div>
<div style={{marginBottom:10}}>
<label style={{...S.head,display:'block',marginBottom:6,fontSize:10}}>CRONTAB AUSDRUCK</label>
<input value={input} onChange={e=>setInput(e.target.value)} placeholder="z.B. 0 8 * * 1"
autoCapitalize="none" style={{...S.inp,fontFamily:"'Courier New',monospace",fontSize:16}}/>
</div>
<div style={{display:'flex',gap:5,flexWrap:'wrap',marginBottom:16}}>
{EXAMPLES.map(([ex,l])=>(
<button key={ex} onClick={()=>setInput(ex)} style={{padding:'4px 10px',borderRadius:6,
fontFamily:'monospace',fontSize:10,cursor:'pointer',
background:'rgba(255,255,255,0.05)',border:'1px solid rgba(255,255,255,0.1)',color:'rgba(255,255,255,0.5)'}}>
<code>{ex}</code> <span style={{color:'rgba(255,255,255,0.5)',fontSize:9}}>({l})</span>
</button>
))}
</div>
{result && (result.ok ? (
<div>
<div style={{background:'rgba(78,205,196,0.08)',border:'1px solid rgba(78,205,196,0.2)',
borderRadius:10,padding:'14px 16px',marginBottom:12}}>
<div style={{color:'rgba(255,255,255,0.5)',fontFamily:'monospace',fontSize:9,marginBottom:5}}>BEDEUTET</div>
<div style={{color:'#fff',fontFamily:'monospace',fontSize:16,fontWeight:700}}>{result.human}</div>
</div>
{result.fields && (
<div style={{display:'grid',gridTemplateColumns:'repeat(5,1fr)',gap:8,marginBottom:12}}>
{[['MIN',0],[' STD',1],['TAG',2],['MON',3],['WTAG',4]].map(([lbl,i])=>{
const raw=expr.split(/\s+/)[i];
const fNames=i===3?MONTHS:i===4?WEEKDAYS:null;
const fd=Object.values(result.fields)[i];
return (
<div key={lbl} style={{background:'rgba(255,255,255,0.04)',border:'1px solid rgba(255,255,255,0.07)',
borderRadius:8,padding:'8px 10px',textAlign:'center'}}>
<div style={{color:'rgba(255,255,255,0.3)',fontFamily:'monospace',fontSize:8,marginBottom:4}}>{lbl}</div>
<div style={{color:'#4ecdc4',fontFamily:"'Courier New',monospace",fontSize:14,fontWeight:700}}>{raw}</div>
<div style={{color:'rgba(255,255,255,0.35)',fontFamily:'monospace',fontSize:9,marginTop:3}}>
{fd?.type==='all'?'jede/r':(describeField(fd,fNames)||'')}
</div>
</div>
);
})}
</div>
)}
{runs.length>0&&(
<div>
<div style={{...S.head,marginBottom:6,fontSize:10}}>NÄCHSTE AUSFÜHRUNGEN</div>
{runs.map((d,i)=>(
<div key={i} style={{display:'flex',alignItems:'center',gap:10,padding:'5px 0',borderBottom:'1px solid rgba(255,255,255,0.04)'}}>
<span style={{color:'rgba(78,205,196,0.5)',fontFamily:'monospace',fontSize:10,minWidth:16,textAlign:'right'}}>{i+1}.</span>
<span style={{color:'rgba(255,255,255,0.7)',fontFamily:'monospace',fontSize:11}}>{fmtDt(d)}</span>
</div>
))}
</div>
)}
</div>
) : (
<div style={{background:'rgba(255,107,157,0.08)',border:'1px solid rgba(255,107,157,0.2)',
borderRadius:8,padding:12,color:'#ff6b9d',fontFamily:'monospace',fontSize:11}}> {result.error}</div>
))}
</div>
);
}
// ── Builder ───────────────────────────────────────────────────────────────────
function CronBuilder() {
const [minute, setMinute] = useState('0');
const [hour, setHour] = useState('8');
const [dom, setDom] = useState('*');
const [month, setMonth] = useState('*');
const [dow, setDow] = useState('*');
const [preset, setPreset] = useState('');
const [copied, setCopied] = useState(false);
const cron = `${minute} ${hour} ${dom} ${month} ${dow}`;
const result = explainCron(cron);
const runs = result?.ok ? nextRuns(cron) : [];
const fmtDt = d => d.toLocaleString('de-DE',{weekday:'short',day:'2-digit',month:'2-digit',hour:'2-digit',minute:'2-digit'});
const copy = async () => { try { await navigator.clipboard.writeText(cron); setCopied(true); setTimeout(()=>setCopied(false),1500); } catch {} };
const PRESETS = [
{k:'every_min', l:'Jede Minute', v:['*','*','*','*','*']},
{k:'every_hour', l:'Jede Stunde', v:['0','*','*','*','*']},
{k:'every_day_8', l:'Tägl. 8:00', v:['0','8','*','*','*']},
{k:'every_mon', l:'Montag 8:00', v:['0','8','*','*','1']},
{k:'every_fri', l:'Freitag 17:00', v:['0','17','*','*','5']},
{k:'weekdays_9', l:'MoFr 9:00', v:['0','9','*','*','1-5']},
{k:'first_month', l:'1. des Monats', v:['0','0','1','*','*']},
{k:'midnight', l:'Mitternacht', v:['0','0','*','*','*']},
];
const applyPreset = p => {
setPreset(p.k);
const [mi,hr,dm,mo,dw] = p.v;
setMinute(mi); setHour(hr); setDom(dm); setMonth(mo); setDow(dw);
};
const Lbl = ({c}) => <div style={{...S.head,marginBottom:4,fontSize:9}}>{c}</div>;
const Sel = ({value,onChange,children}) => (
<select value={value} onChange={e=>{onChange(e.target.value);setPreset('');}}
style={{...S.inp,fontSize:12,padding:'6px 8px',width:'100%'}}>{children}</select>
);
return (
<div>
<div style={{marginBottom:14}}>
<Lbl c="SCHNELLAUSWAHL"/>
<div style={{display:'flex',gap:6,flexWrap:'wrap'}}>
{PRESETS.map(p=>(
<button key={p.k} onClick={()=>applyPreset(p)} style={{
padding:'5px 11px',borderRadius:6,fontFamily:'monospace',fontSize:10,cursor:'pointer',
background:preset===p.k?'rgba(78,205,196,0.15)':'rgba(255,255,255,0.05)',
border:`1px solid ${preset===p.k?'rgba(78,205,196,0.3)':'rgba(255,255,255,0.1)'}`,
color:preset===p.k?'#4ecdc4':'rgba(255,255,255,0.55)',
}}>{p.l}</button>
))}
</div>
</div>
<div style={{display:'grid',gridTemplateColumns:'1fr 1fr',gap:10,marginBottom:14}}>
<div><Lbl c="MINUTE"/>
<Sel value={minute} onChange={setMinute}>
<option value="*">* jede</option>
{[5,10,15,20,30].map(n=><option key={n} value={`*/${n}`}>alle {n} min</option>)}
{Array.from({length:60},(_,i)=><option key={i} value={i}>{String(i).padStart(2,'0')}</option>)}
</Sel>
</div>
<div><Lbl c="STUNDE"/>
<Sel value={hour} onChange={setHour}>
<option value="*">* jede</option>
{Array.from({length:24},(_,i)=><option key={i} value={i}>{String(i).padStart(2,'0')}:00</option>)}
<option value="9,17">9 & 17 Uhr</option>
<option value="8,12,17">8, 12 & 17 Uhr</option>
</Sel>
</div>
<div><Lbl c="WOCHENTAG"/>
<Sel value={dow} onChange={setDow}>
<option value="*">* jeder</option>
<option value="1-5">MoFr (Werktage)</option>
<option value="6,0">Sa & So (Wochenende)</option>
{WEEKDAYS.map((d,i)=><option key={i} value={i}>{d}</option>)}
</Sel>
</div>
<div><Lbl c="TAG DES MONATS"/>
<Sel value={dom} onChange={setDom}>
<option value="*">* jeder</option>
{Array.from({length:31},(_,i)=><option key={i+1} value={i+1}>{i+1}.</option>)}
<option value="1,15">1. & 15.</option>
</Sel>
</div>
<div><Lbl c="MONAT"/>
<Sel value={month} onChange={setMonth}>
<option value="*">* jeder</option>
{MONTHS.map((m,i)=><option key={i+1} value={i+1}>{m}</option>)}
</Sel>
</div>
</div>
<div style={{background:'rgba(0,0,0,0.3)',border:'1px solid rgba(255,255,255,0.1)',borderRadius:10,padding:'14px 16px'}}>
<div style={{display:'flex',alignItems:'center',justifyContent:'space-between',marginBottom:10,flexWrap:'wrap',gap:8}}>
<code style={{color:'#4ecdc4',fontFamily:"'Courier New',monospace",fontSize:22,fontWeight:700,letterSpacing:3}}>{cron}</code>
<button onClick={copy} style={{
background:copied?'rgba(78,205,196,0.15)':'rgba(255,255,255,0.06)',
border:`1px solid ${copied?'rgba(78,205,196,0.3)':'rgba(255,255,255,0.1)'}`,
borderRadius:7,color:copied?'#4ecdc4':'rgba(255,255,255,0.5)',
cursor:'pointer',fontFamily:'monospace',fontSize:11,padding:'6px 14px'}}>
{copied?'✓ Kopiert':'⎘ Kopieren'}
</button>
</div>
{result?.ok&&<div style={{color:'rgba(255,255,255,0.65)',fontFamily:'monospace',fontSize:13,marginBottom:10}}>{result.human}</div>}
{runs.length>0&&(
<div style={{display:'flex',gap:6,flexWrap:'wrap'}}>
{runs.slice(0,3).map((d,i)=>(
<span key={i} style={{background:'rgba(255,255,255,0.04)',borderRadius:5,padding:'3px 8px',
color:'rgba(255,255,255,0.35)',fontFamily:'monospace',fontSize:10}}>{fmtDt(d)}</span>
))}
</div>
)}
</div>
</div>
);
}
// ── Tool-Definitionen ─────────────────────────────────────────────────────────
// ── Elektro-Hilfskomponenten (AUSSERHALB damit keine Remounts) ────────────────
function ElektroInp({ vals, setVals, k, label, unit, placeholder }) {
return (
<div style={{ flex:1, minWidth:100 }}>
<div style={{ color:'rgba(255,255,255,0.55)', fontFamily:'monospace', fontSize:9, marginBottom:3 }}>
{label} [{unit}]
</div>
<input type="number" value={vals[k]||''} onChange={e=>setVals(p=>({...p,[k]:e.target.value}))}
placeholder={placeholder||''}
style={{ ...S.inp, fontSize:13, padding:'7px 9px', width:'100%', boxSizing:'border-box' }}/>
</div>
);
}
function ElektroResult({ label, value, highlight }) {
return (
<div style={{ display:'flex', justifyContent:'space-between', alignItems:'center',
padding:'8px 12px', marginBottom:5, borderRadius:8,
background: highlight ? 'rgba(78,205,196,0.1)' : 'rgba(255,255,255,0.03)',
border: `1px solid ${highlight ? 'rgba(78,205,196,0.25)' : 'rgba(255,255,255,0.07)'}` }}>
<span style={{ color:'rgba(255,255,255,0.55)', fontFamily:'monospace', fontSize:11 }}>{label}</span>
<span style={{ color: highlight ? '#4ecdc4' : '#fff', fontFamily:"'Courier New',monospace",
fontSize:14, fontWeight:700 }}>{value}</span>
</div>
);
}
function ElektroSection({ title, children }) {
return (
<div style={{ marginBottom:20 }}>
<div style={{ ...S.head, marginBottom:10, color:'#4ecdc4' }}>{title}</div>
{children}
</div>
);
}
// ── Elektro-Rechner ───────────────────────────────────────────────────────────
function ElektroRechner({ mobile }) {
const [vals, setVals] = useState({});
const [preis, setPreis] = useState('0.38');
const n = k => { const v = parseFloat(vals[k]); return isNaN(v) ? null : v; };
const fmt = (v, unit, digits=3) => v == null ? '—' : `${parseFloat(v.toPrecision(digits))} ${unit}`;
// Res und Sec als direkte Aliase (kein Render-Problem, keine Inputs)
const cols = mobile ? '1fr' : '1fr 1fr';
// ── Ohmsches Gesetz ──
const U1=n('u1'), I1=n('i1'), R1=n('r1');
const ohmRes = {
U: U1 ?? (I1!=null&&R1!=null ? I1*R1 : null),
I: I1 ?? (U1!=null&&R1!=null ? U1/R1 : null),
R: R1 ?? (U1!=null&&I1!=null ? U1/I1 : null),
};
// ── Leistung P = U * I ──
const U2=n('u2'), I2=n('i2'), P2=n('p2'), R2=n('r2');
const leistRes = (() => {
if (U2!=null&&I2!=null) return { P:U2*I2, U:U2, I:I2, R:U2/I2 };
if (P2!=null&&U2!=null) return { P:P2, I:P2/U2, U:U2, R:U2*U2/P2 };
if (P2!=null&&I2!=null) return { P:P2, U:P2/I2, I:I2, R:P2/(I2*I2) };
if (P2!=null&&R2!=null) return { P:P2, U:Math.sqrt(P2*R2), I:Math.sqrt(P2/R2), R:R2 };
if (U2!=null&&R2!=null) return { P:U2*U2/R2, U:U2, I:U2/R2, R:R2 };
if (I2!=null&&R2!=null) return { P:I2*I2*R2, U:I2*R2, I:I2, R:R2 };
return null;
})();
// ── Energie & Kosten ──
const Pw=n('pw'), Std=n('std'), Tage=n('tage');
const kwhTag = Pw!=null&&Std!=null ? (Pw/1000)*Std : null;
const kwhMonat= kwhTag!=null&&Tage!=null ? kwhTag*Tage : (kwhTag!=null ? kwhTag*30 : null);
const kwhJahr = kwhTag!=null ? kwhTag*365 : null;
const ep = parseFloat(preis);
const kostenTag = kwhTag!=null ? kwhTag*ep : null;
const kostenMonat = kwhMonat!=null ? kwhMonat*ep : null;
const kostenJahr = kwhJahr!=null ? kwhJahr*ep : null;
// ── Wirkungsgrad ──
const Pa=n('pa'), Pe=n('pe');
const eta = Pa!=null&&Pe!=null&&Pe>0 ? (Pa/Pe)*100 : null;
const verlust = Pa!=null&&Pe!=null ? Pe-Pa : null;
// ── Serienschaltung Widerstände ──
const r_ser = ['rs1','rs2','rs3','rs4'].map(k=>n(k)).filter(v=>v!=null);
const R_ser = r_ser.reduce((a,b)=>a+b, 0) || null;
// ── Parallelschaltung Widerstände ──
const r_par = ['rp1','rp2','rp3','rp4'].map(k=>n(k)).filter(v=>v!=null);
const R_par = r_par.length ? 1/(r_par.reduce((a,b)=>a+1/b, 0)) : null;
// ── Kondensator Ladestrom ──
const Cap=n('cap'), Uc=n('uc'), Tc=n('tc');
const Ic = Cap!=null&&Uc!=null&&Tc!=null ? Cap*(Uc/Tc) : null;
return (
<div>
{/* Info */}
<div style={{ color:'rgba(255,255,255,0.5)', fontFamily:'monospace', fontSize:9,
marginBottom:16, lineHeight:1.8 }}>
Felder ausfüllen Ergebnis erscheint automatisch · Mindestens 2 bekannte Größen eingeben
</div>
<div style={{ display:'grid', gridTemplateColumns:cols, gap:20 }}>
<div>
{/* Ohmsches Gesetz */}
<ElektroSection title="⚡ Ohmsches Gesetz U = R · I">
<div style={{ display:'flex', gap:8, marginBottom:10, flexWrap:'wrap' }}>
<ElektroInp vals={vals} setVals={setVals} k="u1" label="Spannung" unit="V" placeholder="230"/>
<ElektroInp vals={vals} setVals={setVals} k="i1" label="Strom" unit="A" placeholder="2"/>
<ElektroInp vals={vals} setVals={setVals} k="r1" label="Widerstand" unit="Ω" placeholder="115"/>
</div>
<ElektroResult label="U Spannung" value={fmt(ohmRes.U,'V')} highlight={!U1&&ohmRes.U!=null}/>
<ElektroResult label="I Strom" value={fmt(ohmRes.I,'A')} highlight={!I1&&ohmRes.I!=null}/>
<ElektroResult label="R Widerstand" value={fmt(ohmRes.R,'Ω')} highlight={!R1&&ohmRes.R!=null}/>
</ElektroSection>
{/* Leistung */}
<ElektroSection title="🔋 Leistung P = U · I = U²/R = I²·R">
<div style={{ display:'flex', gap:8, marginBottom:10, flexWrap:'wrap' }}>
<ElektroInp vals={vals} setVals={setVals} k="u2" label="Spannung" unit="V"/>
<ElektroInp vals={vals} setVals={setVals} k="i2" label="Strom" unit="A"/>
<ElektroInp vals={vals} setVals={setVals} k="p2" label="Leistung" unit="W"/>
<ElektroInp vals={vals} setVals={setVals} k="r2" label="Widerstand" unit="Ω"/>
</div>
{leistRes ? <>
<ElektroResult label="P Leistung" value={fmt(leistRes.P,'W')} highlight={!P2}/>
<ElektroResult label="U Spannung" value={fmt(leistRes.U,'V')} highlight={!U2}/>
<ElektroResult label="I Strom" value={fmt(leistRes.I,'A')} highlight={!I2}/>
<ElektroResult label="R Widerstand" value={fmt(leistRes.R,'Ω')} highlight={!R2}/>
</> : <div style={{color:'rgba(255,255,255,0.45)',fontFamily:'monospace',fontSize:11}}>Mindestens 2 Werte eingeben</div>}
</ElektroSection>
{/* Wirkungsgrad */}
<ElektroSection title="🔄 Wirkungsgrad η = P_ab / P_zu">
<div style={{ display:'flex', gap:8, marginBottom:10 }}>
<ElektroInp vals={vals} setVals={setVals} k="pa" label="Abgabe-Leistung" unit="W" placeholder="800"/>
<ElektroInp vals={vals} setVals={setVals} k="pe" label="Aufnahme-Leistung" unit="W" placeholder="1000"/>
</div>
<ElektroResult label= Wirkungsgrad" value={eta!=null ? `${eta.toFixed(1)} %` : '—'} highlight={eta!=null}/>
<ElektroResult label="Verlustleistung" value={fmt(verlust,'W')}/>
</ElektroSection>
</div>
<div>
{/* Stromkosten */}
<ElektroSection title="💶 Stromkosten">
<div style={{ display:'flex', gap:8, marginBottom:10, flexWrap:'wrap' }}>
<ElektroInp vals={vals} setVals={setVals} k="pw" label="Leistung" unit="W" placeholder="100"/>
<ElektroInp vals={vals} setVals={setVals} k="std" label="Stunden/Tag" unit="h" placeholder="5"/>
<ElektroInp vals={vals} setVals={setVals} k="tage" label="Tage/Monat" unit="d" placeholder="30"/>
</div>
<div style={{ marginBottom:8 }}>
<div style={{ color:'rgba(255,255,255,0.55)', fontFamily:'monospace', fontSize:9, marginBottom:3 }}>
Strompreis [/kWh]
</div>
<input type="number" value={preis} onChange={e=>setPreis(e.target.value)}
step="0.01" min="0"
style={{ ...S.inp, fontSize:13, padding:'7px 9px', width:'100%', boxSizing:'border-box' }}/>
</div>
<ElektroResult label="kWh/Tag" value={kwhTag!=null ? `${kwhTag.toFixed(3)} kWh` : '—'}/>
<ElektroResult label="Kosten/Tag" value={kostenTag!=null ? `${kostenTag.toFixed(3)}` : '—'}/>
<ElektroResult label="kWh/Monat" value={kwhMonat!=null ? `${kwhMonat.toFixed(2)} kWh` : '—'}/>
<ElektroResult label="Kosten/Monat" value={kostenMonat!=null ? `${kostenMonat.toFixed(2)}`: '—'} highlight={kostenMonat!=null}/>
<ElektroResult label="kWh/Jahr" value={kwhJahr!=null ? `${kwhJahr.toFixed(1)} kWh` : '—'}/>
<ElektroResult label="Kosten/Jahr" value={kostenJahr!=null ? `${kostenJahr.toFixed(2)}` : '—'} highlight={kostenJahr!=null}/>
</ElektroSection>
{/* Widerstände */}
<ElektroSection title="🔗 Widerstandsschaltung">
<div style={{ marginBottom:8 }}>
<div style={{ color:'rgba(255,255,255,0.3)', fontFamily:'monospace', fontSize:9, marginBottom:5 }}>SERIE (R = R1+R2+)</div>
<div style={{ display:'flex', gap:6, flexWrap:'wrap', marginBottom:8 }}>
{['rs1','rs2','rs3','rs4'].map(k=><ElektroInp vals={vals} setVals={setVals} key={k} k={k} label={`R${k.slice(2)}`} unit="Ω"/>)}
</div>
<ElektroResult label="Gesamtwiderstand (Serie)" value={r_ser.length>=2 ? fmt(R_ser,'Ω') : '—'} highlight={r_ser.length>=2}/>
</div>
<div>
<div style={{ color:'rgba(255,255,255,0.3)', fontFamily:'monospace', fontSize:9, marginBottom:5 }}>PARALLEL (1/R = 1/R1+1/R2+)</div>
<div style={{ display:'flex', gap:6, flexWrap:'wrap', marginBottom:8 }}>
{['rp1','rp2','rp3','rp4'].map(k=><ElektroInp vals={vals} setVals={setVals} key={k} k={k} label={`R${k.slice(2)}`} unit="Ω"/>)}
</div>
<ElektroResult label="Gesamtwiderstand (Parallel)" value={r_par.length>=2 ? fmt(R_par,'Ω') : '—'} highlight={r_par.length>=2}/>
</div>
</ElektroSection>
{/* Kondensator */}
<ElektroSection title="⚡ Kondensator I = C · (ΔU/Δt)">
<div style={{ display:'flex', gap:8, flexWrap:'wrap', marginBottom:10 }}>
<ElektroInp vals={vals} setVals={setVals} k="cap" label="Kapazität" unit="F" placeholder="0.001"/>
<ElektroInp vals={vals} setVals={setVals} k="uc" label="Spannung" unit="V" placeholder="230"/>
<ElektroInp vals={vals} setVals={setVals} k="tc" label="Zeit Δt" unit="s" placeholder="0.02"/>
</div>
<ElektroResult label="I Ladestrom" value={fmt(Ic,'A')} highlight={Ic!=null}/>
</ElektroSection>
</div>
</div>
{/* Reset */}
<button onClick={()=>setVals({})}
style={{ ...S.btn('#ff6b9d',true), marginTop:8, fontSize:11, padding:'6px 16px' }}>
Alle Felder leeren
</button>
</div>
);
}
// ─────────────────────────────────────────────────────────────── Datetime ──
const MOSES_EPOCH = new Date('1970-01-01T00:00:00Z'); // Moseszeit = Unix time (Sekunden)
function pad2(n){ return String(n).padStart(2,'0'); }
function toLocalISO(d){
return `${d.getFullYear()}-${pad2(d.getMonth()+1)}-${pad2(d.getDate())}T${pad2(d.getHours())}:${pad2(d.getMinutes())}:${pad2(d.getSeconds())}`;
}
function formatDE(d){
return `${pad2(d.getDate())}.${pad2(d.getMonth()+1)}.${d.getFullYear()} ${pad2(d.getHours())}:${pad2(d.getMinutes())}:${pad2(d.getSeconds())}`;
}
function DtRow({ label, value, muted }) {
return (
<div style={{display:'flex',justifyContent:'space-between',alignItems:'center',
padding:'7px 10px',borderRadius:7,background:'rgba(255,255,255,0.03)',marginBottom:4}}>
<span style={{color:'rgba(255,255,255,0.55)',fontFamily:'monospace',fontSize:11}}>{label}</span>
<code style={{color: muted ? 'rgba(255,255,255,0.55)' : '#4ecdc4',fontFamily:"'Courier New',monospace",fontSize:13,fontWeight:700}}>{value}</code>
</div>
);
}
function DatetimeRechner({ mobile }) {
const now = new Date();
// ── Tab 1: Umrechner ──
const [conv, setConv] = useState({ unix:'', local:'', de:'' });
const [convResult, setConvResult] = useState(null);
function convFromUnix(val) {
const n = parseInt(val);
if (isNaN(n)) { setConvResult(null); return; }
const d = new Date(n * 1000);
setConvResult({
de: formatDE(d),
local: toLocalISO(d),
unix: String(n),
wday: WEEKDAYS[d.getDay()],
kw: getKW(d),
});
}
function convFromLocal(val) {
if (!val) { setConvResult(null); return; }
const d = new Date(val);
if (isNaN(d)) { setConvResult(null); return; }
setConvResult({
de: formatDE(d),
local: toLocalISO(d),
unix: String(Math.floor(d.getTime()/1000)),
wday: WEEKDAYS[d.getDay()],
kw: getKW(d),
});
}
function getKW(d) {
const tmp = new Date(Date.UTC(d.getFullYear(), d.getMonth(), d.getDate()));
tmp.setUTCDate(tmp.getUTCDate() + 4 - (tmp.getUTCDay()||7));
const y = new Date(Date.UTC(tmp.getUTCFullYear(),0,1));
return Math.ceil((((tmp-y)/86400000)+1)/7);
}
// ── Tab 2: Geburtstag ──
const [bday, setBday] = useState('');
const [bdayResult, setBdayResult] = useState(null);
function calcBday(val) {
setBday(val);
if (!val) { setBdayResult(null); return; }
const birth = new Date(val);
if (isNaN(birth)) { setBdayResult(null); return; }
const today = new Date();
let age = today.getFullYear() - birth.getFullYear();
const hadBday = today.getMonth() > birth.getMonth() ||
(today.getMonth() === birth.getMonth() && today.getDate() >= birth.getDate());
if (!hadBday) age--;
const nextBday = new Date(today.getFullYear(), birth.getMonth(), birth.getDate());
if (nextBday <= today) nextBday.setFullYear(today.getFullYear()+1);
const diffMs = nextBday - today;
const diffD = Math.ceil(diffMs / 86400000);
const totalD = Math.floor((today - birth) / 86400000);
setBdayResult({ age, diffD, totalD, wday: WEEKDAYS[birth.getDay()],
geburtstag: `${pad2(birth.getDate())}.${pad2(birth.getMonth()+1)}.${birth.getFullYear()}`,
nextDate: `${pad2(nextBday.getDate())}.${pad2(nextBday.getMonth()+1)}.${nextBday.getFullYear()}`,
});
}
// ── Tab 3: Zeitdifferenz ──
const [dtA, setDtA] = useState('');
const [dtB, setDtB] = useState('');
const [diffResult, setDiffResult] = useState(null);
function calcDiff(a, b) {
if (!a || !b) { setDiffResult(null); return; }
const dA = new Date(a), dB = new Date(b);
if (isNaN(dA)||isNaN(dB)) { setDiffResult(null); return; }
const ms = Math.abs(dB - dA);
const s = Math.floor(ms/1000);
const m = Math.floor(s/60);
const h = Math.floor(m/60);
const d = Math.floor(h/24);
const weeks = Math.floor(d/7);
const months = Math.abs(dB.getMonth()-dA.getMonth() + (dB.getFullYear()-dA.getFullYear())*12);
setDiffResult({ ms, s, m, h, d, weeks, months });
}
const inp = { ...S.inp, fontSize:13, padding:'7px 9px', width:'100%', boxSizing:'border-box' };
const lbl = { color:'rgba(255,255,255,0.55)', fontFamily:'monospace', fontSize:10, marginBottom:4 };
const [tab, setTab] = useState('conv');
const tabs = [{id:'conv',label:'⇄ Umrechnen'},{id:'bday',label:'🎂 Geburtstag'},{id:'diff',label:'⏱ Differenz'}];
return (
<div>
<div style={{display:'flex',gap:6,marginBottom:18,flexWrap:'wrap'}}>
{tabs.map(t=>(
<button key={t.id} onClick={()=>setTab(t.id)} style={{
padding:'6px 16px',borderRadius:20,fontFamily:'monospace',fontSize:11,cursor:'pointer',
background:tab===t.id?'#4ecdc4':'rgba(255,255,255,0.05)',
color: tab===t.id?'#0d0d0f':'rgba(255,255,255,0.65)',
border: tab===t.id?'none':'1px solid rgba(255,255,255,0.12)',
fontWeight: tab===t.id?700:400}}>{t.label}</button>
))}
</div>
{tab==='conv' && (
<div>
<div style={{display:'grid',gridTemplateColumns:mobile?'1fr':'1fr 1fr',gap:12,marginBottom:16}}>
<div>
<div style={lbl}>Unix / Moseszeit (Sekunden)</div>
<input style={inp} type="number" placeholder="z.B. 1685554093"
value={conv.unix} onChange={e=>{ setConv(p=>({...p,unix:e.target.value})); convFromUnix(e.target.value); }}/>
</div>
<div>
<div style={lbl}>Datum & Uhrzeit</div>
<input style={inp} type="datetime-local"
value={conv.local} onChange={e=>{ setConv(p=>({...p,local:e.target.value})); convFromLocal(e.target.value); }}/>
</div>
</div>
<button style={{...S.btn('#4ecdc4',false), fontSize:11, padding:'6px 14px', marginBottom:16}}
onClick={()=>{ const d=new Date(); const v=String(Math.floor(d.getTime()/1000)); setConv({unix:v,local:toLocalISO(d),de:''}); convFromUnix(v); }}>
Jetzt verwenden
</button>
{convResult && (
<div>
<DtRow label="Deutsch (DE)" value={convResult.de}/>
<DtRow label="ISO lokal" value={convResult.local}/>
<DtRow label="Unix / Moseszeit" value={convResult.unix}/>
<DtRow label="Wochentag" value={convResult.wday}/>
<DtRow label="Kalenderwoche" value={`KW ${convResult.kw}`} muted/>
</div>
)}
{!convResult && (
<div style={{color:'rgba(255,255,255,0.4)',fontFamily:'monospace',fontSize:11,padding:'10px 0'}}>
Unix-Timestamp oder Datum eingeben alle Formate werden berechnet
</div>
)}
<div style={{marginTop:14,padding:'10px 12px',borderRadius:8,background:'rgba(255,255,255,0.03)',
border:'1px solid rgba(255,255,255,0.07)'}}>
<div style={{color:'rgba(255,255,255,0.45)',fontFamily:'monospace',fontSize:9,marginBottom:6}}>BEISPIEL</div>
<div style={{color:'rgba(255,255,255,0.6)',fontFamily:'monospace',fontSize:11}}>
Moseszeit <code style={{color:'#4ecdc4'}}>1685554093</code> = <code style={{color:'#4ecdc4'}}>31.05.2026 17:28:13</code>
</div>
</div>
</div>
)}
{tab==='bday' && (
<div>
<div style={{marginBottom:16}}>
<div style={lbl}>Geburtsdatum</div>
<input style={{...inp, maxWidth:220}} type="date" value={bday}
onChange={e=>calcBday(e.target.value)}/>
</div>
{bdayResult && (
<div>
<DtRow label="Geburtsdatum" value={`${bdayResult.geburtstag} (${bdayResult.wday})`}/>
<DtRow label="Alter" value={`${bdayResult.age} Jahre`}/>
<DtRow label="Nächster Geburtstag" value={bdayResult.nextDate}/>
<DtRow label="Tage bis Geburtstag" value={`${bdayResult.diffD} Tage`}/>
<DtRow label="Gelebte Tage" value={`${bdayResult.totalD.toLocaleString('de-DE')} Tage`} muted/>
</div>
)}
{!bdayResult && (
<div style={{color:'rgba(255,255,255,0.4)',fontFamily:'monospace',fontSize:11}}>
Geburtsdatum eingeben
</div>
)}
</div>
)}
{tab==='diff' && (
<div>
<div style={{display:'grid',gridTemplateColumns:mobile?'1fr':'1fr 1fr',gap:12,marginBottom:16}}>
<div>
<div style={lbl}>Von</div>
<input style={inp} type="datetime-local" value={dtA}
onChange={e=>{ setDtA(e.target.value); calcDiff(e.target.value, dtB); }}/>
</div>
<div>
<div style={lbl}>Bis</div>
<input style={inp} type="datetime-local" value={dtB}
onChange={e=>{ setDtB(e.target.value); calcDiff(dtA, e.target.value); }}/>
</div>
</div>
{diffResult && (
<div>
<DtRow label="Millisekunden" value={diffResult.ms.toLocaleString('de-DE')} muted/>
<DtRow label="Sekunden" value={diffResult.s.toLocaleString('de-DE')}/>
<DtRow label="Minuten" value={diffResult.m.toLocaleString('de-DE')}/>
<DtRow label="Stunden" value={diffResult.h.toLocaleString('de-DE')}/>
<DtRow label="Tage" value={diffResult.d.toLocaleString('de-DE')}/>
<DtRow label="Wochen" value={`~${diffResult.weeks}`} muted/>
<DtRow label="Monate" value={`~${diffResult.months}`} muted/>
</div>
)}
{!diffResult && (
<div style={{color:'rgba(255,255,255,0.4)',fontFamily:'monospace',fontSize:11}}>
Zwei Zeitpunkte eingeben
</div>
)}
</div>
)}
</div>
);
}
const TOOL_DEFS = [
{id:'cron', label:'⏱ Crontab', desc:'Crontab erklären und erstellen', ready:true,
tabs:[{id:'explain',label:'Erklären'},{id:'build',label:'Erstellen'}],
components:{explain:CronExplainer,build:CronBuilder}},
{id:'elektro', label:'⚡ Elektro', desc:'Ohm, Leistung, Kosten, Widerstände', ready:true, component:ElektroRechner},
{id:'json', label:'{ } JSON', desc:'JSON formatieren', ready:false},
{id:'base64', label:'🔐 Base64', desc:'Text ↔ Base64', ready:false},
{id:'regex', label:'🔍 Regex', desc:'Regex testen', ready:false},
{id:'diff', label:'± Text Diff', desc:'Texte vergleichen', ready:false},
{id:'jwt', label:'🔑 JWT', desc:'JWT dekodieren', ready:false},
{id:'datetime', label:'📅 Datetime', desc:'Zeiten umrechnen · Geburtstag · Differenz', ready:true, component:DatetimeRechner},
{id:'color', label:'🎨 Farben', desc:'HEX/RGB/HSL umrechnen', ready:false},
];
export default function DevTools({ toast, mobile }) {
const [activeTool, setActiveTool] = useState('cron');
const [activeTab, setActiveTab] = useState('explain');
const active = TOOL_DEFS.find(t=>t.id===activeTool);
const ActiveComp = active?.tabs ? active.components[activeTab] : active?.component;
return (
<div style={{padding:mobile?'14px 14px 90px':'36px 44px',maxWidth:860}}>
<div style={{display:'flex',alignItems:'center',gap:12,marginBottom:20,flexWrap:'wrap'}}>
<h1 style={{color:'#fff',fontFamily:"'Space Mono',monospace",fontSize:mobile?17:22,margin:0}}>Dev-Tools</h1>
<span style={{color:'rgba(255,255,255,0.5)',fontFamily:'monospace',fontSize:10}}>Konverter · Formatter · Helfer</span>
</div>
<div style={{display:'flex',gap:7,flexWrap:'wrap',marginBottom:20}}>
{TOOL_DEFS.map(t=>(
<button key={t.id} title={t.desc}
onClick={()=>{ if(t.ready){setActiveTool(t.id);setActiveTab(t.tabs?.[0]?.id||'');} else toast('Kommt bald 🚧'); }}
style={{padding:'8px 14px',borderRadius:10,fontFamily:'monospace',fontSize:11,cursor:t.ready?'pointer':'default',
background:activeTool===t.id?'#4ecdc4':t.ready?'rgba(255,255,255,0.06)':'rgba(255,255,255,0.02)',
color: activeTool===t.id?'#0d0d0f':t.ready?'rgba(255,255,255,0.65)':'rgba(255,255,255,0.4)',
border: activeTool===t.id?'none':t.ready?'1px solid rgba(255,255,255,0.1)':'1px solid rgba(255,255,255,0.05)',
fontWeight: activeTool===t.id?700:400}}>
{t.label}{!t.ready&&<span style={{marginLeft:4,fontSize:8,opacity:0.4}}>bald</span>}
</button>
))}
</div>
<div style={{...S.card}}>
<div style={{display:'flex',alignItems:'center',gap:10,marginBottom:14,flexWrap:'wrap'}}>
<div style={{...S.head,marginBottom:0}}>{active?.label}</div>
<span style={{color:'rgba(255,255,255,0.5)',fontFamily:'monospace',fontSize:10}}>{active?.desc}</span>
</div>
{active?.tabs&&(
<div style={{display:'flex',gap:6,marginBottom:16}}>
{active.tabs.map(tab=>(
<button key={tab.id} onClick={()=>setActiveTab(tab.id)} style={{
padding:'6px 18px',borderRadius:20,fontFamily:'monospace',fontSize:11,cursor:'pointer',
background:activeTab===tab.id?'#4ecdc4':'rgba(255,255,255,0.05)',
color: activeTab===tab.id?'#0d0d0f':'rgba(255,255,255,0.5)',
border: activeTab===tab.id?'none':'1px solid rgba(255,255,255,0.1)',
fontWeight: activeTab===tab.id?700:400}}>
{tab.label}
</button>
))}
</div>
)}
{ActiveComp&&<ActiveComp toast={toast} mobile={mobile}/>}
</div>
</div>
);
}