'use strict';
const $ = s => document.querySelector(s);
const $$ = s => [...document.querySelectorAll(s)];
let apps = [];
let clipStream = null;
let toastTimer = null;
let csvRows = [];
let csvCurrentDelimiter = ';';
let lastFileSHA256 = '';
function toast(msg) {
const el = $('#toast');
el.textContent = msg;
el.classList.add('show');
clearTimeout(toastTimer);
toastTimer = setTimeout(() => el.classList.remove('show'), 2600);
}
async function api(url, options = {}) {
const res = await fetch(url, options);
if (!res.ok) {
let msg = `HTTP ${res.status}`;
try { const j = await res.json(); if (j.error) msg = j.error; } catch (_) {}
throw new Error(msg);
}
const ct = res.headers.get('content-type') || '';
return ct.includes('application/json') ? res.json() : res.text();
}
function fmtBytes(n) {
const u = ['B','KB','MB','GB','TB']; let i=0, v=Number(n||0);
while (v >= 1024 && i < u.length-1) { v/=1024; i++; }
return `${v.toFixed(i ? 1 : 0)} ${u[i]}`;
}
function fmtDate(s) { try { return new Intl.DateTimeFormat('de-DE',{dateStyle:'short',timeStyle:'medium'}).format(new Date(s)); } catch (_) { return s; } }
function psQuote(v) { return `'${String(v ?? '').replaceAll("'", "''")}'`; }
function downloadBlob(name, bytes, type='application/octet-stream') { const blob = bytes instanceof Blob ? bytes : new Blob([bytes],{type}); const url=URL.createObjectURL(blob); const a=document.createElement('a'); a.href=url; a.download=name; a.click(); setTimeout(()=>URL.revokeObjectURL(url),1000); }
function kvRender(target, obj, labels={}) { const el=typeof target==='string'?$(target):target; el.replaceChildren(); for (const [k,v] of Object.entries(obj)) { if(v===undefined||v===null||v==='') continue; const row=document.createElement('div');row.className='kv-row';const key=document.createElement('div');key.className='kv-key';key.textContent=labels[k]||k;const val=document.createElement('div');val.className='kv-value';val.textContent=Array.isArray(v)?v.join(', '):String(v);row.append(key,val);el.append(row); } }
// -------------------- navigation and launcher --------------------
function openView(name) {
$$('.view').forEach(v => v.classList.toggle('active', v.id === `view-${name}`));
$$('.nav-btn').forEach(b => b.classList.toggle('active', b.dataset.view === name));
if (name === 'clipboard') { loadClipRooms(); loadClipHistory(); startClipStream(); }
if (name === 'files') loadFiles();
}
$$('.nav-btn').forEach(b => b.addEventListener('click', () => openView(b.dataset.view)));
$$('[data-open-view]').forEach(b => b.addEventListener('click', () => openView(b.dataset.openView)));
async function loadStatus() {
try { const s = await api('/api/status'); $('#statusText').textContent = `${s.rooms.length} Räume · ${s.files} Dateien · 16 Werkzeuge`; $('#uploadStatus').textContent = `Maximale Dateigröße: ${fmtBytes(s.max_file_bytes)}.`; }
catch (_) { $('#statusText').textContent = 'Status nicht verfügbar'; }
}
async function loadApps() {
try { apps = await api('/api/apps'); const cats = [...new Set(apps.map(a => a.category).filter(Boolean))].sort(); cats.forEach(c => { const o=document.createElement('option');o.value=c;o.textContent=c;$('#appCategory').append(o); }); renderApps(); }
catch (_) { $('#appsGrid').innerHTML = `
Apps konnten nicht geladen werden.
`; }
}
function renderApps() {
const q = ($('#appSearch').value || '').toLocaleLowerCase('de'); const cat = $('#appCategory').value; const grid=$('#appsGrid'); grid.replaceChildren();
apps.filter(a => (!q || (a.title||'').toLocaleLowerCase('de').includes(q)) && (!cat || a.category===cat)).forEach(a => {
const link=document.createElement('a'); link.className='app-tile'; link.href=a.url; link.target='_blank'; link.rel='noopener noreferrer'; link.style.setProperty('--tile-color',a.color||'#427bbd');
const icon=document.createElement('div');icon.className='app-icon';icon.textContent=a.icon||'↗';
const title=document.createElement('div');title.className='app-title';title.textContent=a.title;
const badge=document.createElement('div');badge.className='app-cat';badge.textContent=a.category||'Anwendung';
link.append(icon,title,badge);grid.append(link);
});
if (!grid.children.length) { const e=document.createElement('div'); e.className='empty'; e.textContent='Keine passenden Anwendungen.'; grid.append(e); }
}
$('#appSearch').addEventListener('input',renderApps); $('#appCategory').addEventListener('change',renderApps);
// -------------------- clipboard --------------------
function room() { return ($('#clipRoom').value || 'default').trim(); }
async function loadClipRooms() {
const list=$('#clipRooms');
try {
const rooms=await api('/api/rooms/details');
list.replaceChildren();
rooms.forEach(info=>list.append(renderRoom(info)));
if(!rooms.length){const e=document.createElement('div');e.className='empty';e.textContent='Noch keine Räume vorhanden. Über „Raum öffnen / anlegen“ kannst du den ersten Raum erstellen.';list.append(e);}
return rooms;
} catch (_) {
list.innerHTML='Raumliste konnte nicht geladen werden.
';
return [];
}
}
function renderRoom(info) {
const row=document.createElement('div'); row.className='room-item';
if(info.name===room()) row.classList.add('active');
const open=document.createElement('button'); open.className='room-open'; open.type='button';
const name=document.createElement('span'); name.className='room-name'; name.textContent=info.name;
const meta=document.createElement('span'); meta.className='room-meta';
const parts=[`${info.count} Einträge`]; if(info.secrets) parts.push(`${info.secrets} Secrets`); if(info.last_active) parts.push(`zuletzt ${fmtDate(info.last_active)}`); meta.textContent=parts.join(' · ');
open.append(name,meta); open.addEventListener('click',()=>openClipRoom(info.name));
const actions=document.createElement('div'); actions.className='room-actions';
const clear=document.createElement('button'); clear.className='btn small'; clear.type='button'; clear.textContent='Leeren';
clear.addEventListener('click',async(e)=>{e.stopPropagation();await clearClipRoom(info.name);});
const del=document.createElement('button'); del.className='btn small danger'; del.type='button'; del.textContent='Löschen';
del.addEventListener('click',async(e)=>{e.stopPropagation();await deleteClipRoom(info.name);});
actions.append(clear,del); row.append(open,actions); return row;
}
async function openClipRoom(name, create=false) {
name=String(name||'').trim(); if(!name) return toast('Raumname fehlt');
try {
if(create) await api('/api/rooms',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({name})});
$('#clipRoom').value=name; $('#clipRoomBadge').textContent=name;
await loadClipHistory(); startClipStream(); await loadClipRooms(); loadStatus();
} catch(e) { toast(e.message); }
}
async function clearClipRoom(name) {
if(!confirm(`Verlauf von „${name}“ wirklich leeren? Der Raum bleibt erhalten.`)) return;
try {
await api(`/api/${encodeURIComponent(name)}/history`,{method:'DELETE'});
if(name===room()) await loadClipHistory();
await loadClipRooms(); loadStatus(); toast(`Raum „${name}“ geleert`);
} catch(e) { toast(e.message); }
}
async function deleteClipRoom(name) {
if(!confirm(`Raum „${name}“ inklusive aller Einträge wirklich dauerhaft löschen?`)) return;
try {
await api(`/api/${encodeURIComponent(name)}`,{method:'DELETE'});
if(name===room()) {
if(clipStream){clipStream.close();clipStream=null;}
const remaining=await api('/api/rooms/details');
const next=remaining[0]?.name || 'default'; $('#clipRoom').value=next; $('#clipRoomBadge').textContent=next; await loadClipHistory(); if(remaining.length) startClipStream();
}
await loadClipRooms(); loadStatus(); toast(`Raum „${name}“ gelöscht`);
} catch(e) { toast(e.message); }
}
async function loadClipHistory() {
const r=room(); $('#clipRoomBadge').textContent=r;
try { const items=await api(`/api/${encodeURIComponent(r)}/history?limit=100`); const list=$('#clipHistory'); list.replaceChildren(); [...items].reverse().forEach(c=>list.append(renderClip(c))); if(!items.length){const e=document.createElement('div');e.className='empty';e.textContent='Noch keine Einträge in diesem Raum.';list.append(e);} }
catch(_){ $('#clipHistory').innerHTML='Verlauf konnte nicht geladen werden.
'; }
}
function renderClip(c) {
const item=document.createElement('div');item.className='clip-item';
const top=document.createElement('div');top.className='clip-top'; const left=document.createElement('span');left.textContent=`${c.type||'text'}${c.author?' · '+c.author:''}`; const right=document.createElement('span');right.textContent=fmtDate(c.created_at);top.append(left,right);
const content=document.createElement('div');content.className='clip-content'; content.textContent=c.secret?'••••••••••••':(c.content||''); if(c.secret)content.classList.add('clip-secret');
const actions=document.createElement('div');actions.className='clip-actions';
if(c.secret){const reveal=document.createElement('button');reveal.className='btn small';reveal.textContent=c.one_time?'Einmalig abrufen':'Secret abrufen';reveal.addEventListener('click',async()=>{try{const full=await api(`/api/${encodeURIComponent(c.room)}/clip/${encodeURIComponent(c.id)}`);content.textContent=full.content;content.classList.remove('clip-secret');await navigator.clipboard.writeText(full.content).catch(()=>{});toast('Secret abgerufen und lokal kopiert');if(full.one_time){setTimeout(loadClipHistory,300);setTimeout(loadClipRooms,300);}}catch(e){toast(e.message)}});actions.append(reveal);} else {const copy=document.createElement('button');copy.className='btn small';copy.textContent='Kopieren';copy.addEventListener('click',async()=>{await navigator.clipboard.writeText(c.content||'');toast('Lokal kopiert');});actions.append(copy);}
if(c.expires_at){const exp=document.createElement('span');exp.className='badge';exp.textContent=`bis ${fmtDate(c.expires_at)}`;actions.append(exp);} item.append(top,content,actions);return item;
}
function startClipStream(){ if(clipStream)clipStream.close(); clipStream=null; try{clipStream=new EventSource(`/api/${encodeURIComponent(room())}/stream`);clipStream.addEventListener('clip',()=>{loadClipHistory();loadClipRooms();});}catch(_){} }
$('#clipRoom').addEventListener('change',()=>openClipRoom(room(),true));
$('#clipOpenRoom').addEventListener('click',()=>openClipRoom(room(),true));
$('#clipRoomsRefresh').addEventListener('click',loadClipRooms);
$('#clipRefresh').addEventListener('click',()=>{loadClipHistory();loadClipRooms();});
$('#clipSubmit').addEventListener('click',async()=>{const r=room();const body={type:'text',content:$('#clipContent').value,author:$('#clipAuthor').value,secret:$('#clipSecret').checked,one_time:$('#clipOneTime').checked,ttl_minutes:Number($('#clipTTL').value||0)};try{await api(`/api/${encodeURIComponent(r)}/clip`,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(body)});$('#clipContent').value='';toast('Eintrag abgelegt');await loadClipHistory();await loadClipRooms();startClipStream();loadStatus();}catch(e){toast(e.message)}});
$('#clipPasteLocal').addEventListener('click',async()=>{try{$('#clipContent').value=await navigator.clipboard.readText();}catch(_){toast('Browser-Zwischenablage konnte nicht gelesen werden')}});
$('#clipClear').addEventListener('click',()=>clearClipRoom(room()));
// -------------------- password --------------------
$('#pwGenerate').addEventListener('click',async()=>{const btn=$('#pwGenerate');btn.disabled=true;try{const count=Math.max(1,Math.min(20,Number($('#pwCount').value||1)));const params=new URLSearchParams({count:String(count)});if($('#pwStore').checked){params.set('room',($('#pwRoom').value||'default').trim());params.set('ttl_minutes',String(Number($('#pwTTL').value||0)));params.set('one_time',String($('#pwOneTime').checked));params.set('secret','true');}const data=await api(`/api/generate?${params}`,{method:'POST'});$('#pwOutput').textContent=data.map(x=>x.password).join('\n');const meta=$('#pwMeta');meta.replaceChildren();const ent=document.createElement('span');ent.className='meta-pill';ent.textContent=`Entropy ≈ ${data[0]?.entropy_bits?.toFixed(1)||'–'} bit`;meta.append(ent);if(data.some(x=>x.stored)){const p=document.createElement('span');p.className='meta-pill';p.textContent=`Im Raum ${$('#pwRoom').value||'default'} abgelegt`;meta.append(p);}toast('Passwort erzeugt');loadStatus();}catch(e){$('#pwOutput').textContent=`Fehler: ${e.message}`;}finally{btn.disabled=false;}});
$('#pwCopy').addEventListener('click',async()=>{try{await navigator.clipboard.writeText($('#pwOutput').textContent);toast('Lokal kopiert');}catch(_){toast('Kopieren fehlgeschlagen')}});
// -------------------- file transfer --------------------
async function loadFiles(){try{const files=await api('/api/files');const list=$('#filesList');list.replaceChildren();files.forEach(f=>list.append(renderFile(f)));if(!files.length){const e=document.createElement('div');e.className='empty';e.textContent='Noch keine Dateien vorhanden.';list.append(e);}loadStatus();}catch(_){$('#filesList').innerHTML='Dateiliste konnte nicht geladen werden.
';}}
function renderFile(f){const row=document.createElement('div');row.className='file-row';const a=document.createElement('div');const n=document.createElement('div');n.className='file-name';n.textContent=f.name;const sub=document.createElement('div');sub.className='file-sub';sub.textContent=`${fmtBytes(f.size)} · ${fmtDate(f.uploaded_at)}${f.uploader?' · '+f.uploader:''}`;a.append(n,sub);const b=document.createElement('div');const h=document.createElement('div');h.className='file-hash';h.textContent=`SHA-256 ${f.sha256}`;b.append(h);const actions=document.createElement('div');actions.className='file-actions';const dl=document.createElement('a');dl.className='btn small';dl.href=`/api/files/${encodeURIComponent(f.id)}`;dl.textContent='Download';const del=document.createElement('button');del.className='btn small danger';del.textContent='Löschen';del.addEventListener('click',async()=>{if(!confirm(`„${f.name}“ wirklich löschen?`))return;try{await api(`/api/files/${encodeURIComponent(f.id)}`,{method:'DELETE'});toast('Datei gelöscht');loadFiles();}catch(e){toast(e.message)}});actions.append(dl,del);row.append(a,b,actions);return row;}
$('#uploadFile').addEventListener('change',()=>{$('#filePicked').textContent=$('#uploadFile').files[0]?.name||'Keine Datei ausgewählt';});
$('#uploadForm').addEventListener('submit',async(e)=>{e.preventDefault();const file=$('#uploadFile').files[0];if(!file)return;const form=new FormData();form.append('file',file);form.append('uploader',$('#fileUploader').value);const btn=e.currentTarget.querySelector('button[type=submit]');btn.disabled=true;$('#uploadStatus').textContent='Upload läuft…';try{const m=await api('/api/files',{method:'POST',body:form});$('#uploadStatus').textContent=`Hochgeladen: ${m.name} · SHA-256 ${m.sha256}`;$('#uploadFile').value='';$('#filePicked').textContent='Keine Datei ausgewählt';toast('Datei hochgeladen');loadFiles();}catch(err){$('#uploadStatus').textContent=`Fehler: ${err.message}`;}finally{btn.disabled=false;}});
$('#filesRefresh').addEventListener('click',loadFiles);
// -------------------- tools navigation --------------------
function openTool(name) { $$('.tool-panel').forEach(p=>p.classList.toggle('active',p.id===`tool-${name}`)); $$('.tool-nav-btn').forEach(b=>b.classList.toggle('active',b.dataset.tool===name)); }
$$('.tool-nav-btn').forEach(b=>b.addEventListener('click',()=>openTool(b.dataset.tool)));
$('#toolSearch').addEventListener('input',()=>{const q=$('#toolSearch').value.toLocaleLowerCase('de').trim(); const buttons=$$('.tool-nav-btn'); buttons.forEach(b=>b.hidden=!!q&&!`${b.textContent} ${b.dataset.keywords||''}`.toLocaleLowerCase('de').includes(q)); const active=buttons.find(b=>b.classList.contains('active')); if(active?.hidden){const first=buttons.find(b=>!b.hidden);if(first)openTool(first.dataset.tool);}});
// -------------------- onboarding assistant --------------------
function asciiName(v) { return String(v||'').normalize('NFD').replace(/[\u0300-\u036f]/g,'').replace(/ß/g,'ss').replace(/[^A-Za-z0-9.-]/g,'').toLowerCase(); }
function buildOnboarding() {
const first=$('#onbFirst').value.trim(), last=$('#onbLast').value.trim(); if(!first||!last) throw new Error('Vorname und Nachname fehlen');
const sam=(asciiName(first).slice(0,1)+asciiName(last)).slice(0,20); const domain=$('#onbDomain').value.trim(); const mailDomain=($('#onbMailDomain').value.trim()||domain); const display=$('#onbDisplayMode').value==='lastfirst'?`${last}, ${first}`:`${first} ${last}`; const upn=domain?`${sam}@${domain}`:sam; const mail=mailDomain?`${asciiName(first)}.${asciiName(last)}@${mailDomain}`:'';
$('#onbSam').textContent=sam;$('#onbUPN').textContent=upn;$('#onbMail').textContent=mail||'–';$('#onbDisplay').textContent=display;
const args=[`-Name ${psQuote(display)}`,`-GivenName ${psQuote(first)}`,`-Surname ${psQuote(last)}`,`-DisplayName ${psQuote(display)}`,`-SamAccountName ${psQuote(sam)}`,`-UserPrincipalName ${psQuote(upn)}`];
const optional=[['#onbOU','-Path'],['#onbDepartment','-Department'],['#onbTitle','-Title'],['#onbOffice','-Office']]; optional.forEach(([id,arg])=>{const v=$(id).value.trim();if(v)args.push(`${arg} ${psQuote(v)}`);}); if(mail)args.push(`-EmailAddress ${psQuote(mail)}`);
args.push('-AccountPassword $InitialPassword','-Enabled $true','-ChangePasswordAtLogon $true');
const bt=String.fromCharCode(96);
let ps="# Kennwort separat aus dem PAW-Secret-Transfer übernehmen\n$InitialPassword = Read-Host 'Initiales Kennwort' -AsSecureString\n\nNew-ADUser "+bt+"\n "+args.join(" "+bt+"\n ")+"\n";
const groups=$('#onbGroups').value.split(',').map(x=>x.trim()).filter(Boolean); if(groups.length){ps+='\n# Gruppenmitgliedschaften\n'+groups.map(g=>`Add-ADGroupMember -Identity ${psQuote(g)} -Members ${psQuote(sam)}`).join('\n')+'\n';}
$('#onbPS').value=ps; return {sam,upn,mail,display};
}
$('#onbBuild').addEventListener('click',()=>{try{buildOnboarding();toast('Onboarding-Daten erzeugt')}catch(e){toast(e.message)}});
$('#onbCopy').addEventListener('click',async()=>{try{if(!$('#onbPS').value)buildOnboarding();await navigator.clipboard.writeText($('#onbPS').value);toast('PowerShell kopiert')}catch(e){toast(e.message)}});
$('#onbPassword').addEventListener('click',async()=>{try{const data=buildOnboarding();const params=new URLSearchParams({count:'1',room:($('#onbRoom').value||'onboarding').trim(),ttl_minutes:String(Number($('#onbTTL').value||15)),one_time:'true',secret:'true'});const r=await api(`/api/generate?${params}`,{method:'POST'});$('#onbSecretStatus').textContent=`Kennwort für ${data.sam} als einmaliges Secret abgelegt · Raum ${$('#onbRoom').value||'onboarding'} · Clip ${r[0].clip_id}.`;toast('Kennwort erzeugt und als Secret abgelegt');loadStatus();}catch(e){toast(e.message)}});
// -------------------- file/hash inspector --------------------
$('#fiFile').addEventListener('change',()=>$('#fiPicked').textContent=$('#fiFile').files[0]?.name||'Keine Datei ausgewählt');
$('#fiForm').addEventListener('submit',async e=>{e.preventDefault();const f=$('#fiFile').files[0];if(!f)return;const fd=new FormData();fd.append('file',f);try{const x=await api('/api/tools/file-inspect',{method:'POST',body:fd});lastFileSHA256=x.sha256;kvRender('#fiResult',x,{name:'Dateiname',size:'Größe (Bytes)',mime:'Erkannter MIME-Type',extension:'Erweiterung',magic_hex:'Erste Bytes (Hex)',md5:'MD5',sha256:'SHA-256',sha512:'SHA-512'});}catch(err){toast(err.message)}});
$('#fiCopyHash').addEventListener('click',async()=>{if(!lastFileSHA256)return toast('Zuerst Datei analysieren');await navigator.clipboard.writeText(lastFileSHA256);toast('SHA-256 kopiert')});
// -------------------- certificate inspector --------------------
$('#certFile').addEventListener('change',()=>$('#certPicked').textContent=$('#certFile').files[0]?.name||'Keine Datei ausgewählt');
$('#certForm').addEventListener('submit',async e=>{e.preventDefault();const f=$('#certFile').files[0];if(!f)return;const fd=new FormData();fd.append('file',f);fd.append('password',$('#certPassword').value);const out=$('#certResult');out.textContent='Analyse läuft…';try{const x=await api('/api/tools/cert-inspect',{method:'POST',body:fd});out.replaceChildren();x.certificates.forEach((c,i)=>{const card=document.createElement('section');card.className='cert-card';const h=document.createElement('h3');h.textContent=`Zertifikat ${i+1}${c.expired?' · ABGELAUFEN':''}`; if(c.expired)h.classList.add('status-bad');card.append(h);const kv=document.createElement('div');kv.className='kv-list';kvRender(kv,{subject:c.subject,issuer:c.issuer,serial:c.serial,dns_names:c.dns_names,ip_addresses:c.ip_addresses,emails:c.emails,not_before:fmtDate(c.not_before),not_after:fmtDate(c.not_after),days_left:c.days_left,is_ca:c.is_ca,signature_algorithm:c.signature_algorithm,public_key_algorithm:c.public_key_algorithm,key_usage:c.key_usage,ext_key_usage:c.ext_key_usage,sha1_thumbprint:c.sha1_thumbprint,sha256_thumbprint:c.sha256_thumbprint},{subject:'Subject',issuer:'Issuer',serial:'Seriennummer',dns_names:'DNS SANs',ip_addresses:'IP SANs',emails:'E-Mail SANs',not_before:'Gültig ab',not_after:'Gültig bis',days_left:'Tage verbleibend',is_ca:'CA-Zertifikat',signature_algorithm:'Signaturalgorithmus',public_key_algorithm:'Public-Key-Algorithmus',key_usage:'Key Usage',ext_key_usage:'Extended Key Usage',sha1_thumbprint:'SHA-1 Thumbprint',sha256_thumbprint:'SHA-256 Thumbprint'});card.append(kv);out.append(card);});}catch(err){out.textContent='';toast(err.message)}});
// -------------------- formatter --------------------
function formatXML(xml, compact=false) { const parser=new DOMParser();const doc=parser.parseFromString(xml,'application/xml');const err=doc.querySelector('parsererror');if(err)throw new Error(err.textContent.split('\n')[0]||'Ungültiges XML');let s=new XMLSerializer().serializeToString(doc);if(compact)return s;s=s.replace(/>\s*<').replace(/(>)(<)(\/?)/g,'$1\n$2$3');let depth=0;return s.split('\n').map(line=>{const t=line.trim();if(/^<\//.test(t))depth=Math.max(0,depth-1);const out=' '.repeat(depth)+t;if(/^<[^!?/][^>]*[^/]?>$/.test(t)&&!/<\/[^>]+>$/.test(t))depth++;return out;}).join('\n'); }
function basicYAML(text, action) { const lines=text.replace(/\r\n/g,'\n').split('\n');for(let i=0;ix.trimEnd()).filter(x=>x.trim()!=='').join('\n');return lines.map(x=>x.replace(/[ \t]+$/,'')).join('\n').trim()+"\n"; }
$('#fmtRun').addEventListener('click',()=>{try{const type=$('#fmtType').value,act=$('#fmtAction').value,input=$('#fmtInput').value;let out='';if(type==='JSON'){const o=JSON.parse(input);out=act==='validate'?'JSON ist syntaktisch gültig.':JSON.stringify(o,null,act==='pretty'?2:0);}else if(type==='XML'){out=act==='validate'?(formatXML(input,true),'XML ist syntaktisch gültig.'):formatXML(input,act==='minify');}else out=basicYAML(input,act);$('#fmtOutput').value=out;$('#fmtStatus').textContent=type==='YAML'?'YAML-Basisprüfung/Normalisierung lokal abgeschlossen.':'Lokale Verarbeitung erfolgreich.';}catch(e){$('#fmtOutput').value='';$('#fmtStatus').textContent=`Fehler: ${e.message}`;}});
$('#fmtCopy').addEventListener('click',async()=>{await navigator.clipboard.writeText($('#fmtOutput').value);toast('Ergebnis kopiert')});
// -------------------- text diff --------------------
function lineDiff(a,b){const A=a.replace(/\r\n/g,'\n').split('\n'),B=b.replace(/\r\n/g,'\n').split('\n');if(A.length>500||B.length>500)throw new Error('Diff ist auf 500 Zeilen je Seite begrenzt');const dp=Array.from({length:A.length+1},()=>new Uint16Array(B.length+1));for(let i=A.length-1;i>=0;i--)for(let j=B.length-1;j>=0;j--)dp[i][j]=A[i]===B[j]?dp[i+1][j+1]+1:Math.max(dp[i+1][j],dp[i][j+1]);let i=0,j=0,out=[];while(i=dp[i+1][j])){out.push(['+',B[j++]]);}else{out.push(['-',A[i++]]);}}return out;}
$('#diffRun').addEventListener('click',()=>{const out=$('#diffOutput');out.replaceChildren();try{for(const [t,line] of lineDiff($('#diffA').value,$('#diffB').value)){const s=document.createElement('span');s.className=t==='+'?'diff-add':t==='-'?'diff-del':'diff-same';s.textContent=`${t} ${line}\n`;out.append(s);}}catch(e){out.textContent=`Fehler: ${e.message}`;}});
// -------------------- AD converters --------------------
function newUUID(){const b=crypto.getRandomValues(new Uint8Array(16));b[6]=(b[6]&15)|64;b[8]=(b[8]&63)|128;const h=[...b].map(x=>x.toString(16).padStart(2,'0'));return `${h.slice(0,4).join('')}-${h.slice(4,6).join('')}-${h.slice(6,8).join('')}-${h.slice(8,10).join('')}-${h.slice(10).join('')}`;}
function guidToAD(g){const raw=g.replace(/[{}-]/g,'').toLowerCase();if(!/^[0-9a-f]{32}$/.test(raw))throw new Error('GUID/Hex muss 16 Bytes enthalten');const b=raw.match(/../g);const ad=[...b.slice(0,4).reverse(),...b.slice(4,6).reverse(),...b.slice(6,8).reverse(),...b.slice(8)];return {hex:ad.join(''),ldap:ad.map(x=>'\\'+x).join('')};}
function adHexToGuid(h){const raw=h.replace(/[^0-9a-f]/gi,'').toLowerCase();if(raw.length!==32)throw new Error('AD-Hex muss 16 Bytes enthalten');const b=raw.match(/../g);const n=[...b.slice(0,4).reverse(),...b.slice(4,6).reverse(),...b.slice(6,8).reverse(),...b.slice(8)];return `${n.slice(0,4).join('')}-${n.slice(4,6).join('')}-${n.slice(6,8).join('')}-${n.slice(8,10).join('')}-${n.slice(10).join('')}`;}
$('#guidNew').addEventListener('click',()=>{$('#guidInput').value=newUUID();$('#guidOutput').textContent=''});
$('#guidConvert').addEventListener('click',()=>{try{const v=$('#guidInput').value.trim();if(v.includes('-')){const x=guidToAD(v);$('#guidOutput').textContent=`AD objectGUID Hex:\n${x.hex}\n\nLDAP escaped:\n${x.ldap}`;}else{$('#guidOutput').textContent=`GUID aus AD-Bytefolge:\n${adHexToGuid(v)}`;}}catch(e){$('#guidOutput').textContent=`Fehler: ${e.message}`;}});
function sidStringToHex(s){const p=s.trim().split('-');if(p[0]!=='S'||p.length<4)throw new Error('Ungültige SID');const rev=Number(p[1]),auth=BigInt(p[2]),subs=p.slice(3).map(BigInt);if(subs.length>255)throw new Error('Zu viele Subauthorities');const out=[rev,subs.length];for(let i=5;i>=0;i--)out.push(Number((auth>>BigInt(i*8))&255n));for(const x of subs)for(let i=0;i<4;i++)out.push(Number((x>>BigInt(i*8))&255n));return out.map(x=>x.toString(16).padStart(2,'0')).join('');}
function sidHexToString(h){const raw=h.replace(/[^0-9a-f]/gi,'');if(raw.length<16||raw.length%2)throw new Error('Ungültiges SID-Hex');const b=raw.match(/../g).map(x=>parseInt(x,16));const rev=b[0],cnt=b[1];if(b.length<8+cnt*4)throw new Error('SID-Hex zu kurz');let auth=0n;for(let i=2;i<8;i++)auth=(auth<<8n)|BigInt(b[i]);const subs=[];let o=8;for(let n=0;n{try{const v=$('#sidInput').value.trim();$('#sidOutput').textContent=v.toUpperCase().startsWith('S-')?`Binär/Hex (little endian SubAuthorities):\n${sidStringToHex(v)}`:`SID:\n${sidHexToString(v)}`;}catch(e){$('#sidOutput').textContent=`Fehler: ${e.message}`;}});
function parseTimeInput(v){v=v.trim();const gen=v.match(/^(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})(?:\.\d+)?Z$/);if(gen)return new Date(Date.UTC(+gen[1],+gen[2]-1,+gen[3],+gen[4],+gen[5],+gen[6]));if(/^\d+$/.test(v)){const n=BigInt(v);if(n>1000000000000000n){const ms=Number(n/10000n-11644473600000n);return new Date(ms);}if(n>100000000000n)return new Date(Number(n));return new Date(Number(n)*1000);}const d=new Date(v);if(Number.isNaN(d.getTime()))throw new Error('Zeitwert nicht erkannt');return d;}
function formatGeneralized(d){const p=n=>String(n).padStart(2,'0');return `${d.getUTCFullYear()}${p(d.getUTCMonth()+1)}${p(d.getUTCDate())}${p(d.getUTCHours())}${p(d.getUTCMinutes())}${p(d.getUTCSeconds())}.0Z`;}
function convertTime(){try{const d=parseTimeInput($('#timeInput').value);const ms=BigInt(d.getTime());const ft=(ms+11644473600000n)*10000n;$('#timeOutput').textContent=`ISO 8601: ${d.toISOString()}\nLokal: ${d.toLocaleString('de-DE')}\nUnix Sekunden: ${Math.floor(d.getTime()/1000)}\nUnix Millisekunden: ${d.getTime()}\nWindows FILETIME / AD: ${ft}\nLDAP GeneralizedTime: ${formatGeneralized(d)}`;}catch(e){$('#timeOutput').textContent=`Fehler: ${e.message}`;}}
$('#timeNow').addEventListener('click',()=>{$('#timeInput').value=new Date().toISOString();convertTime();});$('#timeConvert').addEventListener('click',convertTime);
// -------------------- DNS / subnet / connectivity --------------------
$('#dnsRun').addEventListener('click',async()=>{const out=$('#dnsOutput');out.textContent='Abfrage läuft…';try{const x=await api('/api/tools/dns',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({name:$('#dnsName').value,type:$('#dnsType').value})});out.textContent=`${x.type} ${x.name} · ${x.duration_ms} ms\n\n${x.results.length?x.results.join('\n'):'Keine Ergebnisse'}`;}catch(e){out.textContent=`Fehler: ${e.message}`;}});
$('#subnetRun').addEventListener('click',async()=>{try{const x=await api('/api/tools/subnet',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({cidr:$('#subnetInput').value})});kvRender('#subnetOutput',x,{input:'Eingabe',network:'Netz',prefix_length:'Präfix',address_bits:'Adressbits',family:'Familie',netmask:'Netzmaske',broadcast:'Broadcast',first_address:'Erste Adresse',last_address:'Letzte Adresse',first_host:'Erster Host',last_host:'Letzter Host',address_count:'Adressen gesamt',usable_hosts:'Nutzbare Hosts'});}catch(e){toast(e.message)}});
$('#connRun').addEventListener('click',async()=>{try{const x=await api('/api/tools/connectivity',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({host:$('#connHost').value,port:Number($('#connPort').value),timeout_ms:Number($('#connTimeout').value)})});kvRender('#connOutput',x,{host:'Host',port:'Port',resolved_ips:'DNS-Auflösung',latency_ms:'TCP-Latenz ms',reachable:'Erreichbar',remote:'Remote Endpoint',error:'Fehler'});const first=$('#connOutput .kv-row:nth-child(5) .kv-value');if(first)first.classList.add(x.reachable?'status-ok':'status-bad');}catch(e){toast(e.message)}});
// -------------------- encoder --------------------
function bytesToBase64(bytes){let s='';const step=0x8000;for(let i=0;ic.charCodeAt(0));}
function encoderRun(){try{const type=$('#encType').value,mode=$('#encMode').value,input=$('#encInput').value;let out='';if(type==='url')out=mode==='encode'?encodeURIComponent(input):decodeURIComponent(input);else if(type==='base64')out=mode==='encode'?bytesToBase64(new TextEncoder().encode(input)):new TextDecoder().decode(base64ToBytes(input));else if(mode==='encode')out=[...new TextEncoder().encode(input)].map(b=>b.toString(16).padStart(2,'0')).join('');else{const h=input.replace(/\s+/g,'');if(!/^[0-9a-f]*$/i.test(h)||h.length%2)throw new Error('Ungültiges Hex');out=new TextDecoder().decode(Uint8Array.from(h.match(/../g)||[],x=>parseInt(x,16)));}$('#encOutput').value=out;}catch(e){$('#encOutput').value=`Fehler: ${e.message}`;}}
$('#encRun').addEventListener('click',encoderRun);$('#encSwap').addEventListener('click',()=>{const x=$('#encInput').value;$('#encInput').value=$('#encOutput').value;$('#encOutput').value=x;$('#encMode').value=$('#encMode').value==='encode'?'decode':'encode';});
// -------------------- PowerShell builder --------------------
function psBuild(){
const t=$('#psTemplate').value,id=$('#psIdentity').value.trim(),g=$('#psGroup').value.trim(),f=$('#psFirst').value.trim(),l=$('#psLast').value.trim(),ou=$('#psOU').value.trim();
let s='';
switch(t){
case 'getuser': s=`Get-ADUser -Identity ${psQuote(id)} -Properties *`; break;
case 'unlock': s=`Unlock-ADAccount -Identity ${psQuote(id)}`; break;
case 'addgroup': s=`Add-ADGroupMember -Identity ${psQuote(g)} -Members ${psQuote(id)}`; break;
case 'resetpw': s=`$NewPassword = Read-Host 'Neues Kennwort' -AsSecureString\nSet-ADAccountPassword -Identity ${psQuote(id)} -Reset -NewPassword $NewPassword\nSet-ADUser -Identity ${psQuote(id)} -ChangePasswordAtLogon $true`; break;
case 'newuser': {
const display=`${l}, ${f}`.replace(/^, |, $/,'');
const args=[`-Name ${psQuote(display)}`,`-GivenName ${psQuote(f)}`,`-Surname ${psQuote(l)}`];
if(id)args.push(`-SamAccountName ${psQuote(id)}`); if(ou)args.push(`-Path ${psQuote(ou)}`);
const bt=String.fromCharCode(96);
s="$InitialPassword = Read-Host 'Initiales Kennwort' -AsSecureString\nNew-ADUser "+bt+"\n "+args.join(" "+bt+"\n ")+" "+bt+"\n -AccountPassword $InitialPassword -Enabled $true -ChangePasswordAtLogon $true";
break;
}
}
$('#psOutput').textContent=s; return s;
}
$('#psBuild').addEventListener('click',()=>{psBuild();toast('Befehl erzeugt')});$('#psCopy').addEventListener('click',async()=>{await navigator.clipboard.writeText(psBuild());toast('PowerShell kopiert')});
// -------------------- CSV viewer --------------------
function detectDelimiter(text){const first=(text.split(/\r?\n/).find(x=>x.trim())||'');const cand=[';',',','\t'];return cand.map(d=>[d,(first.match(new RegExp(d==='\t'?'\\t':`\\${d}`,'g'))||[]).length]).sort((a,b)=>b[1]-a[1])[0][0];}
function parseCSV(text,delim){const rows=[];let row=[],field='',quoted=false;for(let i=0;i!q||r.some(c=>c.toLocaleLowerCase('de').includes(q)));const table=document.createElement('table');table.className='data-table';const tr=document.createElement('tr');head.forEach(h=>{const th=document.createElement('th');th.textContent=h;tr.append(th)});const thead=document.createElement('thead');thead.append(tr);table.append(thead);const body=document.createElement('tbody');data.slice(0,1000).forEach(r=>{const tr=document.createElement('tr');for(let i=0;i1000?' · Anzeige auf 1000 Zeilen begrenzt':''}`;}
$('#csvFile').addEventListener('change',async()=>{const f=$('#csvFile').files[0];$('#csvPicked').textContent=f?.name||'Keine Datei ausgewählt';if(!f)return;const text=await f.text();const sel=$('#csvDelimiter').value;csvCurrentDelimiter=sel==='auto'?detectDelimiter(text):sel;csvRows=parseCSV(text,csvCurrentDelimiter);renderCSV();});$('#csvFilter').addEventListener('input',renderCSV);$('#csvDelimiter').addEventListener('change',()=>{$('#csvFile').dispatchEvent(new Event('change'))});
function csvEscape(v,d){v=String(v??'');return /["\r\n]/.test(v)||v.includes(d)?`"${v.replaceAll('"','""')}"`:v;}
$('#csvExport').addEventListener('click',()=>{if(!csvRows.length)return toast('Keine CSV geladen');const q=$('#csvFilter').value.toLocaleLowerCase('de');const rows=[csvRows[0],...csvRows.slice(1).filter(r=>!q||r.some(c=>c.toLocaleLowerCase('de').includes(q)))];downloadBlob('filtered.csv','\uFEFF'+rows.map(r=>r.map(v=>csvEscape(v,csvCurrentDelimiter)).join(csvCurrentDelimiter)).join('\r\n'),'text/csv;charset=utf-8');});
// -------------------- regex --------------------
$('#regexRun').addEventListener('click',()=>{const out=$('#regexOutput');try{const flags=$('#regexFlags').value.replace(/[^dgimsuvy]/g,'');const global=flags.includes('g')?flags:flags+'g';const re=new RegExp($('#regexPattern').value,global);const text=$('#regexText').value;const matches=[...text.matchAll(re)];out.textContent=matches.length?matches.slice(0,200).map((m,i)=>`#${i+1} index=${m.index} ${JSON.stringify(m[0])}${m.length>1?' groups='+JSON.stringify(m.slice(1)):''}`).join('\n'):'Keine Treffer.';}catch(e){out.textContent=`Fehler: ${e.message}`;}});
// -------------------- text transformer --------------------
$('#transformRun').addEventListener('click',()=>{let lines=$('#transformInput').value.replace(/\r\n/g,'\n').split('\n');const a=$('#transformAction').value,x=$('#transformAffix').value;switch(a){case'trim':lines=lines.map(s=>s.trim());break;case'sort':lines.sort((a,b)=>a.localeCompare(b,'de',{numeric:true}));break;case'unique':lines=[...new Set(lines)];break;case'upper':lines=lines.map(s=>s.toLocaleUpperCase('de'));break;case'lower':lines=lines.map(s=>s.toLocaleLowerCase('de'));break;case'prefix':lines=lines.map(s=>x+s);break;case'suffix':lines=lines.map(s=>s+x);break;case'reverse':lines.reverse();break;}$('#transformOutput').value=lines.join('\n');});
// -------------------- ZIP archive viewer --------------------
$('#archiveFile').addEventListener('change',()=>$('#archivePicked').textContent=$('#archiveFile').files[0]?.name||'Keine Datei ausgewählt');
$('#archiveForm').addEventListener('submit',async e=>{e.preventDefault();const f=$('#archiveFile').files[0];if(!f)return;const fd=new FormData();fd.append('file',f);try{const x=await api('/api/tools/archive',{method:'POST',body:fd});$('#archiveMeta').textContent=`${x.entry_count} Einträge · komprimiert ${fmtBytes(x.compressed_bytes)} · entpackt ${fmtBytes(x.uncompressed_bytes)}`;const wrap=$('#archiveTable');wrap.replaceChildren();const table=document.createElement('table');table.className='data-table';const head=document.createElement('tr');['Name','Größe','Komprimiert','Typ','Geändert'].forEach(x=>{const th=document.createElement('th');th.textContent=x;head.append(th)});const thead=document.createElement('thead');thead.append(head);table.append(thead);const body=document.createElement('tbody');x.entries.forEach(en=>{const tr=document.createElement('tr');[en.name,fmtBytes(en.size),fmtBytes(en.compressed_size),en.directory?'Verzeichnis':`ZIP method ${en.method}`,fmtDate(en.modified)].forEach(v=>{const td=document.createElement('td');td.textContent=v;tr.append(td)});body.append(tr)});table.append(body);wrap.append(table);}catch(err){toast(err.message)}});
// -------------------- encoding converter --------------------
let encodingOriginalName='converted.txt';
$('#encodingFile').addEventListener('change',async()=>{const f=$('#encodingFile').files[0];$('#encodingPicked').textContent=f?.name||'Keine Datei ausgewählt';if(!f)return;encodingOriginalName=f.name;try{const b=await f.arrayBuffer();$('#encodingText').value=new TextDecoder($('#encodingSource').value).decode(b);}catch(e){toast(`Encoding konnte nicht gelesen werden: ${e.message}`)}});
$('#encodingSource').addEventListener('change',()=>{$('#encodingFile').dispatchEvent(new Event('change'))});
$('#encodingDownload').addEventListener('click',()=>{let bytes=new TextEncoder().encode($('#encodingText').value);if($('#encodingTarget').value==='utf8bom'){const out=new Uint8Array(bytes.length+3);out.set([0xef,0xbb,0xbf]);out.set(bytes,3);bytes=out;}downloadBlob(encodingOriginalName.replace(/(\.[^.]*)?$/,'.utf8$1'),bytes,'text/plain;charset=utf-8');});
loadStatus();
loadApps();