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 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','Mo–Fr 18:30'],['0 9,17 * * *','9 & 17 Uhr'],['0 0 * * 0','So 0:00']]; return (
setInput(e.target.value)} placeholder="z.B. 0 8 * * 1" autoCapitalize="none" style={{...S.inp,fontFamily:"'Courier New',monospace",fontSize:16}}/>
{EXAMPLES.map(([ex,l])=>( ))}
{result && (result.ok ? (
BEDEUTET
{result.human}
{result.fields && (
{[['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 (
{lbl}
{raw}
{fd?.type==='all'?'jede/r':(describeField(fd,fNames)||'–')}
); })}
)} {runs.length>0&&(
NÄCHSTE AUSFÜHRUNGEN
{runs.map((d,i)=>(
{i+1}. {fmtDt(d)}
))}
)}
) : (
⚠ {result.error}
))}
); } // ── 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:'Mo–Fr 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}) =>
{c}
; const Sel = ({value,onChange,children}) => ( ); return (
{PRESETS.map(p=>( ))}
{[5,10,15,20,30].map(n=>)} {Array.from({length:60},(_,i)=>)}
{Array.from({length:24},(_,i)=>)}
{WEEKDAYS.map((d,i)=>)}
{Array.from({length:31},(_,i)=>)}
{MONTHS.map((m,i)=>)}
{cron}
{result?.ok&&
{result.human}
} {runs.length>0&&(
{runs.slice(0,3).map((d,i)=>( {fmtDt(d)} ))}
)}
); } // ── Tool-Definitionen ───────────────────────────────────────────────────────── // ── Elektro-Hilfskomponenten (AUSSERHALB damit keine Remounts) ──────────────── function ElektroInp({ vals, setVals, k, label, unit, placeholder }) { return (
{label} [{unit}]
setVals(p=>({...p,[k]:e.target.value}))} placeholder={placeholder||''} style={{ ...S.inp, fontSize:13, padding:'7px 9px', width:'100%', boxSizing:'border-box' }}/>
); } function ElektroResult({ label, value, highlight }) { return (
{label} {value}
); } function ElektroSection({ title, children }) { return (
{title}
{children}
); } // ── 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 (
{/* Info */}
Felder ausfüllen → Ergebnis erscheint automatisch · Mindestens 2 bekannte Größen eingeben
{/* Ohmsches Gesetz */}
{/* Leistung */}
{leistRes ? <> :
Mindestens 2 Werte eingeben
}
{/* Wirkungsgrad */}
{/* Stromkosten */}
Strompreis [€/kWh]
setPreis(e.target.value)} step="0.01" min="0" style={{ ...S.inp, fontSize:13, padding:'7px 9px', width:'100%', boxSizing:'border-box' }}/>
{/* Widerstände */}
SERIE (R = R1+R2+…)
{['rs1','rs2','rs3','rs4'].map(k=>)}
=2 ? fmt(R_ser,'Ω') : '—'} highlight={r_ser.length>=2}/>
PARALLEL (1/R = 1/R1+1/R2+…)
{['rp1','rp2','rp3','rp4'].map(k=>)}
=2 ? fmt(R_par,'Ω') : '—'} highlight={r_par.length>=2}/>
{/* Kondensator */}
{/* Reset */}
); } // ─────────────────────────────────────────────────────────────── Datetime ── // Moseszeit-Formel: moses + 94687200 = Unix-Timestamp (Sekunden) // Anzeige immer als Europe/Berlin Lokalzeit (CEST=+2, CET=+1) const MOSES_TO_UNIX_OFFSET = 94687200; // Sekunden function pad2(n){ return String(n).padStart(2,'0'); } function mosesToUnixMs(moses) { return (moses + MOSES_TO_UNIX_OFFSET) * 1000; } function unixMsToMoses(unixMs) { return Math.floor(unixMs / 1000) - MOSES_TO_UNIX_OFFSET; } // Date -> Berlin-Lokalzeit-String "DD.MM.YYYY HH:MM:SS" function formatBerlin(d) { return d.toLocaleString('de-DE', { timeZone: 'Europe/Berlin', year:'numeric', month:'2-digit', day:'2-digit', hour:'2-digit', minute:'2-digit', second:'2-digit', hour12: false, }).replace(',',''); } // Date -> ISO-String in Berlin-Zeit für datetime-local input (YYYY-MM-DDTHH:MM:SS) function toLocalISO(d) { const s = d.toLocaleString('sv-SE', { timeZone: 'Europe/Berlin' }); // sv-SE gibt YYYY-MM-DD HH:MM:SS return s.replace(' ', 'T'); } 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 (
{label} {value}
); } 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 unixMs = n * 1000; const d = new Date(unixMs); const moses = unixMsToMoses(unixMs); setConvResult({ de: formatBerlin(d), local: toLocalISO(d), unix: String(n), moses: String(moses), wday: d.toLocaleDateString('de-DE', { timeZone:'Europe/Berlin', weekday:'long' }), kw: getKW(d), }); } function convFromMoses(val) { const n = parseInt(val); if (isNaN(n)) { setConvResult(null); return; } const unixMs = mosesToUnixMs(n); const d = new Date(unixMs); setConvResult({ de: formatBerlin(d), local: toLocalISO(d), unix: String(Math.floor(unixMs/1000)), moses: String(n), wday: d.toLocaleDateString('de-DE', { timeZone:'Europe/Berlin', weekday:'long' }), kw: getKW(d), }); } function convFromLocal(val) { if (!val) { setConvResult(null); return; } // datetime-local gibt lokale Browser-Zeit — wir interpretieren als Europe/Berlin const d = new Date(val); // browser local if (isNaN(d)) { setConvResult(null); return; } const unixMs = d.getTime(); setConvResult({ de: formatBerlin(d), local: val, unix: String(Math.floor(unixMs/1000)), moses: String(unixMsToMoses(unixMs)), wday: d.toLocaleDateString('de-DE', { timeZone:'Europe/Berlin', weekday:'long' }), 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 ageY = today.getFullYear() - birth.getFullYear(); let ageM = today.getMonth() - birth.getMonth(); let ageD = today.getDate() - birth.getDate(); if (ageD < 0) { ageM--; const prev = new Date(today.getFullYear(), today.getMonth(), 0); ageD += prev.getDate(); } if (ageM < 0) { ageY--; ageM += 12; } const nextBday = new Date(today.getFullYear(), birth.getMonth(), birth.getDate()); if (nextBday <= today) nextBday.setFullYear(today.getFullYear()+1); const diffMs = nextBday - today; const diffD = (diffMs / 86400000).toFixed(1); const totalD = ((today - birth) / 86400000).toFixed(1); setBdayResult({ ageStr: `${ageY} Jahre, ${ageM} Monate, ${ageD} Tage`, 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()} (${WEEKDAYS[nextBday.getDay()]})`, }); } // ── Tab 3: Zeitdifferenz ── const [dtA, setDtA] = useState(''); const [dtATime, setDtATime] = useState(''); const [dtB, setDtB] = useState(''); const [dtBTime, setDtBTime] = useState(''); const [diffResult, setDiffResult] = useState(null); function calcDiff(a, aTime, b, bTime) { if (!a || !b) { setDiffResult(null); return; } const strA = aTime ? `${a}T${aTime}:00` : `${a}T00:00:00`; const strB = bTime ? `${b}T${bTime}:00` : `${b}T00:00:00`; const dA = new Date(strA), dB = new Date(strB); if (isNaN(dA)||isNaN(dB)) { setDiffResult(null); return; } const hasTime = !!(aTime || bTime); const ms = Math.abs(dB - dA); const s = (ms / 1000).toFixed(1); const m = (ms / 60000).toFixed(1); const h = (ms / 3600000).toFixed(2); const d = (ms / 86400000).toFixed(2); const weeks = (ms / (86400000*7)).toFixed(2); const dA2 = new Date(Math.min(dA,dB)), dB2 = new Date(Math.max(dA,dB)); const months = Math.abs(dB2.getMonth()-dA2.getMonth() + (dB2.getFullYear()-dA2.getFullYear())*12); setDiffResult({ ms, s, m, h, d, weeks, months, hasTime }); } 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 (
{tabs.map(t=>( ))}
{tab==='conv' && (
Moseszeit
{ setConv(p=>({...p,moses:e.target.value})); convFromMoses(e.target.value); }}/>
Unix-Timestamp
{ setConv(p=>({...p,unix:e.target.value})); convFromUnix(e.target.value); }}/>
Datum & Uhrzeit (Berlin)
{ setConv(p=>({...p,local:e.target.value})); convFromLocal(e.target.value); }}/>
{convResult && (
)} {!convResult && (
Einen der drei Werte eingeben → alle anderen werden berechnet
)}
FORMEL
Moses + 94.687.200 = Unix  ·  Moseszeit 1685554093 = Unix 1780241293 = 31.05.2026 17:28:13
)} {tab==='bday' && (
Geburtsdatum
calcBday(e.target.value)}/>
{!!bday && }
{bdayResult && (
)} {!bdayResult && (
Geburtsdatum eingeben
)}
)} {tab==='diff' && (
Von – Datum
{ setDtA(e.target.value); calcDiff(e.target.value, dtATime, dtB, dtBTime); }}/>
Von – Uhrzeit (optional)
{ setDtATime(e.target.value); calcDiff(dtA, e.target.value, dtB, dtBTime); }}/>
Bis – Datum
{ setDtB(e.target.value); calcDiff(dtA, dtATime, e.target.value, dtBTime); }}/>
Bis – Uhrzeit (optional)
{ setDtBTime(e.target.value); calcDiff(dtA, dtATime, dtB, e.target.value); }}/>
{!!(dtA||dtB||dtATime||dtBTime) && } {diffResult && (
{!!diffResult.hasTime && } {!!diffResult.hasTime && } {!!diffResult.hasTime && } {!!diffResult.hasTime && }
)} {!diffResult && (
Zwei Datumswerte eingeben · Uhrzeit ist optional
)}
)}
); } // ─────────────────────────────────────────────────────────────── Key-Gen ── const KEY_TYPES = [ { id: 'laravel', label: 'Laravel APP_KEY', desc: 'base64, 32 Byte', example: 'APP_KEY= in .env bei Laravel-Projekten – verschlüsselt Sessions & Daten', gen: () => 'base64:' + btoa(String.fromCharCode(...crypto.getRandomValues(new Uint8Array(32)))) }, { id: 'jwt_hs256', label: 'JWT Secret (HS256)', desc: '64 Byte hex', example: 'JWT_SECRET= in Node.js/Express-APIs – signiert Login-Tokens', gen: () => hex(64) }, { id: 'jwt_hs512', label: 'JWT Secret (HS512)', desc: '128 Byte hex', example: 'Wie HS256, aber stärker – für sicherheitskritische APIs empfohlen', gen: () => hex(128) }, { id: 'aes256', label: 'AES-256 Key', desc: '32 Byte base64', example: 'Symmetrische Verschlüsselung von Dateien oder Datenbank-Feldern', gen: () => b64(32) }, { id: 'aes128', label: 'AES-128 Key', desc: '16 Byte base64', example: 'Leichtgewichtige Verschlüsselung, z.B. für eingebettete Systeme', gen: () => b64(16) }, { id: 'secret32', label: 'Generic Secret (32B)', desc: '32 Byte hex', example: 'NEXTAUTH_SECRET=, SESSION_SECRET= oder ähnliche Umgebungsvariablen', gen: () => hex(32) }, { id: 'secret64', label: 'Generic Secret (64B)', desc: '64 Byte hex', example: 'Webhook-Signaturen (z.B. GitHub, Stripe) oder API-Signing-Keys', gen: () => hex(64) }, { id: 'passwd', label: 'Passwort (stark)', desc: '20 Zeichen, alle Typen', example: 'Admin-Passwörter, Service-Accounts oder initiale DB-Root-Passwörter', gen: () => genPasswd(20) }, { id: 'passwd16', label: 'Passwort (kurz)', desc: '16 Zeichen, alle Typen', example: 'Nutzer-Initialpasswörter oder Zugänge mit Zeichenlimit', gen: () => genPasswd(16) }, { id: 'uuid', label: 'UUID v4', desc: 'zufällige UUID', example: 'Eindeutige IDs für Datenbank-Einträge, Dateinamen oder API-Ressourcen', gen: () => crypto.randomUUID() }, { id: 'nanoid', label: 'NanoID (21)', desc: 'URL-safe, 21 Zeichen', example: 'Kurze IDs in URLs (z.B. share-Links) – kompakter als UUID', gen: () => nanoid(21) }, { id: 'nanoid32', label: 'NanoID (32)', desc: 'URL-safe, 32 Zeichen', example: 'Session-IDs oder Token in URLs ohne Sonderzeichen-Probleme', gen: () => nanoid(32) }, { id: 'docker_pw', label: 'DB-Passwort (Docker)', desc: '24 Zeichen, alnum+sonder', example: 'MYSQL_ROOT_PASSWORD=, POSTGRES_PASSWORD= in docker-compose.yml', gen: () => genPasswd(24) }, { id: 'hex16', label: 'Hex Token (16B)', desc: '32 Hex-Zeichen', example: 'Kurze API-Keys, CSRF-Tokens oder Einmal-Codes', gen: () => hex(16) }, { id: 'hex32', label: 'Hex Token (32B)', desc: '64 Hex-Zeichen', example: 'Passwort-Reset-Links, E-Mail-Verifikations-Tokens', gen: () => hex(32) }, { id: 'b64_32', label: 'Base64 Token (32B)', desc: '32 Byte base64', example: 'Cookie-Signing-Keys oder kompakte Secrets in YAML/JSON-Configs', gen: () => b64(32) }, { id: 'b64_64', label: 'Base64 Token (64B)', desc: '64 Byte base64', example: 'VAPID-Keys (Web Push), OAuth Client Secrets', gen: () => b64(64) }, { id: 'b64url_32', label: 'Base64url Token (32B)', desc: '32 Byte base64url', example: 'OAuth2 state/code_verifier, JWT-kompatible Token ohne Padding', gen: () => b64url(32) }, ]; function hex(bytes) { return Array.from(crypto.getRandomValues(new Uint8Array(bytes))).map(b=>b.toString(16).padStart(2,'0')).join(''); } function b64(bytes) { return btoa(String.fromCharCode(...crypto.getRandomValues(new Uint8Array(bytes)))); } function b64url(bytes) { return b64(bytes).replace(/\+/g,'-').replace(/\//g,'_').replace(/=+$/,''); } function nanoid(len) { const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_'; const rnd = crypto.getRandomValues(new Uint8Array(len)); return Array.from(rnd).map(b => chars[b % 64]).join(''); } function genPasswd(len) { const sets = ['ABCDEFGHIJKLMNOPQRSTUVWXYZ','abcdefghijklmnopqrstuvwxyz','0123456789','!@#$%^&*()-_=+[]{}']; const all = sets.join(''); const rnd = crypto.getRandomValues(new Uint8Array(len + 8)); let result = sets.map(s => s[rnd[sets.indexOf(s)] % s.length]); // guarantee 1 from each set let idx = 4; while (result.length < len) { result.push(all[rnd[idx++ % rnd.length] % all.length]); } // shuffle for (let i = result.length - 1; i > 0; i--) { const j = rnd[(i * 3) % rnd.length] % (i + 1); [result[i], result[j]] = [result[j], result[i]]; } return result.join(''); } function KeyGen({ toast }) { const [keys, setKeys] = useState({}); const [copied, setCopied] = useState(null); function generate(id) { const kt = KEY_TYPES.find(k => k.id === id); if (!kt) return; setKeys(p => ({ ...p, [id]: kt.gen() })); } function generateAll() { const all = {}; KEY_TYPES.forEach(kt => { all[kt.id] = kt.gen(); }); setKeys(all); } function clearAll() { setKeys({}); } async function copyKey(id, val) { try { await navigator.clipboard.writeText(val); setCopied(id); setTimeout(() => setCopied(null), 1800); } catch { toast('Kopieren fehlgeschlagen'); } } return (
{!!Object.keys(keys).length && ( )}
{KEY_TYPES.map(kt => (
{kt.label} {kt.desc}
{kt.example}
{!!keys[kt.id] && ( )}
{!!keys[kt.id] && (
copyKey(kt.id, keys[kt.id])} title="Klicken zum Kopieren"> {keys[kt.id]} {copied===kt.id ? '✓' : '⎘'}
)}
))}
Alle Keys werden lokal im Browser generiert (Web Crypto API) · Nichts wird gespeichert · Beim Seitenwechsel verschwinden sie
); } // ─────────────────────────────────────────────────────────────── Text Diff ── function diffLines(a, b) { const aLines = a.split('\n'); const bLines = b.split('\n'); const m = aLines.length, n = bLines.length; const dp = Array.from({length: m+1}, () => new Array(n+1).fill(0)); for (let i = m-1; i >= 0; i--) for (let j = n-1; j >= 0; j--) dp[i][j] = aLines[i] === bLines[j] ? dp[i+1][j+1]+1 : Math.max(dp[i+1][j], dp[i][j+1]); // Build rows: same / del / add — consecutive del+add stay paired (same display line number) const rows = []; // {type, lineA, lineB, text} let i = 0, j = 0, lnA = 0, lnB = 0; while (i < m || j < n) { if (i < m && j < n && aLines[i] === bLines[j]) { lnA++; lnB++; rows.push({type:'same', ln: lnA, text: aLines[i]}); i++; j++; } else if (j < n && (i >= m || dp[i][j+1] >= dp[i+1][j])) { lnB++; rows.push({type:'add', ln: lnB, text: bLines[j]}); j++; } else { lnA++; rows.push({type:'del', ln: lnA, text: aLines[i]}); i++; } } return rows; } // Which line numbers (1-based) in text A/B are changed function changedLineNums(rows, side) { const set = new Set(); rows.forEach(r => { if (r.type === 'del' && side === 'a') set.add(r.ln); if (r.type === 'add' && side === 'b') set.add(r.ln); }); return set; } function TextDiff({ mobile }) { const [left, setLeft] = useState(''); const [right, setRight] = useState(''); const [diff, setDiff] = useState(null); function compare() { if (!left.trim() && !right.trim()) return; setDiff(diffLines(left, right)); } function clear() { setLeft(''); setRight(''); setDiff(null); } function loadFile(e, setter) { const file = e.target.files?.[0]; if (!file) return; const reader = new FileReader(); reader.onload = ev => { setter(ev.target.result); setDiff(null); }; reader.readAsText(file); e.target.value = ''; } const added = diff ? diff.filter(d=>d.type==='add').length : 0; const removed = diff ? diff.filter(d=>d.type==='del').length : 0; const same = diff ? diff.filter(d=>d.type==='same').length : 0; const changedA = diff ? changedLineNums(diff, 'a') : new Set(); const changedB = diff ? changedLineNums(diff, 'b') : new Set(); const ta = { ...S.inp, fontSize:12, fontFamily:"'Courier New',monospace", width:'100%', boxSizing:'border-box', minHeight:180, resize:'vertical', lineHeight:'1.6', padding:'7px 8px', whiteSpace:'pre', overflowX:'auto', overflowWrap:'normal' }; function LineNumCol({ text, changedSet }) { return (
{(text||' ').split('\n').map((_,i) => (
{i+1}
))}
); } return (
{([['left','Text A (Original)', left, setLeft, changedA], ['right','Text B (Neu)', right, setRight, changedB]]).map(([side, label, val, setter, cset])=>(
{label}