diff --git a/frontend/src/tools/devtools.jsx b/frontend/src/tools/devtools.jsx
index 04671af..defccb6 100644
--- a/frontend/src/tools/devtools.jsx
+++ b/frontend/src/tools/devtools.jsx
@@ -939,6 +939,411 @@ function KeyGen({ toast }) {
);
}
+// ─────────────────────────────────────────────────────────────── Text Diff ──
+
+function diffLines(a, b) {
+ const aLines = a.split('\n');
+ const bLines = b.split('\n');
+ // Myers diff (simple LCS-based)
+ 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]);
+
+ const result = [];
+ let i = 0, j = 0;
+ while (i < m || j < n) {
+ if (i < m && j < n && aLines[i] === bLines[j]) {
+ result.push({ type: 'same', text: aLines[i] }); i++; j++;
+ } else if (j < n && (i >= m || dp[i][j+1] >= dp[i+1][j])) {
+ result.push({ type: 'add', text: bLines[j] }); j++;
+ } else {
+ result.push({ type: 'del', text: aLines[i] }); i++;
+ }
+ }
+ return result;
+}
+
+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 ta = { ...S.inp, fontSize:12, fontFamily:"'Courier New',monospace", width:'100%',
+ boxSizing:'border-box', minHeight:180, resize:'vertical', lineHeight:1.6 };
+
+ return (
+
+
+ {[['left','Text A (Original)', left, setLeft], ['right','Text B (Neu)', right, setRight]].map(([side, label, val, setter])=>(
+
+
+ {label}
+
+
+
+ ))}
+
+
+
+
+ {!!(left||right||diff) && (
+
+ )}
+ {!!diff && (
+
+ − {removed} entfernt
+ + {added} hinzugefügt
+ = {same} gleich
+
+ )}
+
+
+ {!!diff && (
+
+
+
+ {diff.map((d, i) => (
+
+ |
+ {d.type==='add'?'+': d.type==='del'?'−':''}
+ |
+
+ {d.text || ' '}
+ |
+
+ ))}
+
+
+
+ )}
+ {!diff && (
+
+ Texte eingeben oder Dateien laden → Vergleichen klicken
+
+ )}
+
+ );
+}
+
+// ──────────────────────────────────────────────────────────── Netzwerk ──
+
+function NetzwerkRechner({ mobile }) {
+ const [nTab, setNTab] = useState('speed');
+
+ // ── Geschwindigkeit ──
+ const [speedVal, setSpeedVal] = useState('');
+ const [speedUnit, setSpeedUnit] = useState('mbps');
+
+ const SPEED_UNITS = [
+ { id:'bps', label:'bit/s', toBps: 1 },
+ { id:'kbps', label:'kbit/s', toBps: 1e3 },
+ { id:'mbps', label:'Mbit/s', toBps: 1e6 },
+ { id:'gbps', label:'Gbit/s', toBps: 1e9 },
+ { id:'bBps', label:'B/s', toBps: 8 },
+ { id:'kBps', label:'kB/s', toBps: 8e3 },
+ { id:'mBps', label:'MB/s', toBps: 8e6 },
+ { id:'gBps', label:'GB/s', toBps: 8e9 },
+ ];
+
+ function fmtSpeed(bps, u) {
+ const v = bps / u.toBps;
+ if (v >= 1000) return v.toLocaleString('de-DE', {maximumFractionDigits:2}) + ' ' + u.label;
+ return parseFloat(v.toPrecision(5)).toLocaleString('de-DE') + ' ' + u.label;
+ }
+
+ const speedBps = parseFloat(speedVal) * (SPEED_UNITS.find(u=>u.id===speedUnit)?.toBps||1e6);
+ const speedValid = !isNaN(speedBps) && speedVal !== '';
+
+ // Download-Zeit
+ function dlTime(bps, sizeBytes) {
+ const s = (sizeBytes * 8) / bps;
+ if (s < 60) return `${s.toFixed(1)} Sek.`;
+ if (s < 3600) return `${(s/60).toFixed(1)} Min.`;
+ return `${(s/3600).toFixed(2)} Std.`;
+ }
+
+ // ── IP / Subnetz ──
+ const [ipInput, setIpInput] = useState('');
+ const [subnetResult, setSubnetResult] = useState(null);
+ const [subnetErr, setSubnetErr] = useState('');
+
+ function parseIP(str) {
+ const parts = str.trim().split('.').map(Number);
+ if (parts.length !== 4 || parts.some(p=>isNaN(p)||p<0||p>255)) return null;
+ return parts.reduce((acc,p)=>(acc<<8)|p, 0) >>> 0;
+ }
+
+ function calcSubnet(input) {
+ setIpInput(input);
+ setSubnetErr('');
+ setSubnetResult(null);
+ if (!input.trim()) return;
+ let ip, prefix;
+
+ if (input.includes('/')) {
+ const [ipPart, pfx] = input.split('/');
+ ip = parseIP(ipPart);
+ prefix = parseInt(pfx);
+ if (ip === null || isNaN(prefix) || prefix < 0 || prefix > 32) { setSubnetErr('Ungültige CIDR-Notation'); return; }
+ } else {
+ ip = parseIP(input);
+ if (ip === null) { setSubnetErr('Ungültige IP-Adresse'); return; }
+ prefix = 24;
+ }
+
+ const mask = prefix === 0 ? 0 : (~0 << (32 - prefix)) >>> 0;
+ const net = (ip & mask) >>> 0;
+ const bcast = (net | (~mask >>> 0)) >>> 0;
+ const hosts = prefix >= 31 ? (prefix===32?1:2) : Math.pow(2, 32-prefix) - 2;
+ const first = prefix >= 31 ? net : (net + 1) >>> 0;
+ const last = prefix >= 31 ? bcast : (bcast - 1) >>> 0;
+
+ function i2ip(n) { return [(n>>>24)&0xff,(n>>>16)&0xff,(n>>>8)&0xff,n&0xff].join('.'); }
+ function i2mask(n) { return i2ip(n); }
+
+ // Klasse
+ const first8 = (ip>>>24)&0xff;
+ const klasse = first8 < 128?'A': first8<192?'B': first8<224?'C': first8<240?'D (Multicast)':'E (Reserviert)';
+ const privat = (first8===10)||(first8===172&&((ip>>>16)&0xff)>=16&&((ip>>>16)&0xff)<=31)||
+ (first8===192&&((ip>>>16)&0xff)===168);
+
+ setSubnetResult({
+ ip: i2ip(ip), cidr: `${i2ip(net)}/${prefix}`,
+ mask: i2mask(mask), maskBin: mask.toString(2).padStart(32,'0').match(/.{8}/g).join('.'),
+ net: i2ip(net), bcast: i2ip(bcast),
+ first: i2ip(first), last: i2ip(last),
+ hosts: hosts.toLocaleString('de-DE'),
+ prefix, klasse, privat,
+ wildcard: i2ip(~mask>>>0),
+ });
+ }
+
+ const CIDR_TABLE = [
+ {pfx:'/8', mask:'255.0.0.0', hosts:'16.777.214', bsp:'Klasse A – Großnetz'},
+ {pfx:'/16',mask:'255.255.0.0',hosts:'65.534',bsp:'Klasse B – Mittelnetz'},
+ {pfx:'/24',mask:'255.255.255.0',hosts:'254',bsp:'Typisches Heimnetz'},
+ {pfx:'/25',mask:'255.255.255.128',hosts:'126',bsp:'Hälfte eines /24'},
+ {pfx:'/26',mask:'255.255.255.192',hosts:'62',bsp:'Viertel eines /24'},
+ {pfx:'/27',mask:'255.255.255.224',hosts:'30',bsp:'Kleines Subnetz'},
+ {pfx:'/28',mask:'255.255.255.240',hosts:'14',bsp:'VLAN, kleine Abteilung'},
+ {pfx:'/29',mask:'255.255.255.248',hosts:'6',bsp:'Point-to-Point + Reserve'},
+ {pfx:'/30',mask:'255.255.255.252',hosts:'2',bsp:'Point-to-Point Link'},
+ {pfx:'/32',mask:'255.255.255.255',hosts:'1',bsp:'Einzelner Host / Loopback'},
+ ];
+
+ const nTabs = [{id:'speed',label:'⚡ Geschwindigkeit'},{id:'subnet',label:'🌐 IP / Subnetz'},{id:'cidr',label:'📋 CIDR-Tabelle'}];
+ const lbl = {color:'rgba(255,255,255,0.55)',fontFamily:'monospace',fontSize:10,marginBottom:4};
+ const inp = {...S.inp, fontSize:13, padding:'7px 9px', width:'100%', boxSizing:'border-box'};
+
+ function NRow({label,value,muted,mono}) {
+ return (
+
+ {label}
+ {value}
+
+ );
+ }
+
+ return (
+
+
+ {nTabs.map(t=>(
+
+ ))}
+
+
+ {nTab==='speed' && (
+
+
+
+
Wert eingeben
+
setSpeedVal(e.target.value)}/>
+
+
+
Einheit
+
+
+
+ {speedValid && (
+ <>
+
+ {SPEED_UNITS.map(u=>(
+
+ ))}
+
+
+
DOWNLOAD-ZEITEN BEI DIESER GESCHWINDIGKEIT
+ {[['700 MB CD',700e6],['4,7 GB DVD',4.7e9],['25 GB Blu-ray',25e9],['50 GB Spiel',50e9],['100 GB',100e9]].map(([lbl,b])=>(
+
+ ))}
+
+
+
+ 💡 Faustregel: Mbit/s ÷ 8 = MB/s · Internetanbieter werben mit Mbit/s, Dateimanager zeigt MB/s
+
+
+ >
+ )}
+ {!speedValid && (
+
+ Geschwindigkeit eingeben → alle Einheiten werden berechnet
+
+ )}
+
+ )}
+
+ {nTab==='subnet' && (
+
+
+
IP-Adresse oder CIDR (z.B. 192.168.1.100/24)
+
+ calcSubnet(e.target.value)}/>
+ {!!ipInput && }
+
+ {!!subnetErr &&
{subnetErr}
}
+
+ {subnetResult && (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 💡 Subnetzmaske: 1er-Bits = Netzanteil, 0er-Bits = Hostanteil ·
+ Broadcast = letzte Adresse · Netz = erste Adresse
+
+
+
+ )}
+ {!subnetResult && !subnetErr && (
+
+ IP eingeben (mit oder ohne /Präfix) → Subnetz wird berechnet · Standard: /24
+
+ )}
+
+ )}
+
+ {nTab==='cidr' && (
+
+
+
+
+
+ {['Präfix','Subnetzmaske','Nutzbare Hosts','Typischer Einsatz'].map(h=>(
+ | {h} |
+ ))}
+
+
+
+ {CIDR_TABLE.map((r,i)=>(
+
+ | {r.pfx} |
+ {r.mask} |
+ {r.hosts} |
+ {r.bsp} |
+
+ ))}
+
+
+
+
+
PRIVATE ADRESSBEREICHE (RFC 1918)
+ {[
+ ['10.0.0.0/8','10.0.0.0 – 10.255.255.255','Klasse A, große Firmennetzwerke'],
+ ['172.16.0.0/12','172.16.0.0 – 172.31.255.255','Klasse B, mittlere Netzwerke'],
+ ['192.168.0.0/16','192.168.0.0 – 192.168.255.255','Klasse C, Heimnetzwerke'],
+ ].map(([cidr,range,desc])=>(
+
+ {cidr}
+ {range}
+ {desc}
+
+ ))}
+
+
+ )}
+
+ );
+}
+
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'}],
@@ -946,7 +1351,8 @@ const TOOL_DEFS = [
{id:'elektro', label:'⚡ Elektro', desc:'Ohm, Leistung, Kosten, Widerstände', ready:true, component:ElektroRechner},
{id:'json', label:'{ } JSON', desc:'JSON formatieren', ready:false},
{id:'regex', label:'🔍 Regex', desc:'Regex testen', ready:false},
- {id:'diff', label:'± Text Diff', desc:'Texte vergleichen', ready:false},
+ {id:'diff', label:'± Text Diff', desc:'Zwei Texte vergleichen', ready:true, component:TextDiff},
+ {id:'netzwerk', label:'🌐 Netzwerk', desc:'IP, Subnetz, Geschwindigkeit, CIDR', ready:true, component:NetzwerkRechner},
{id:'datetime', label:'📅 Datetime', desc:'Zeiten umrechnen · Geburtstag · Differenz', ready:true, component:DatetimeRechner},
{id:'keygen', label:'🔐 Key-Gen', desc:'Sichere Keys & Secrets generieren', ready:true, component:KeyGen},
];