Files
pawtoolbox/web/app.js
2026-08-26 07:30:25 +02:00

282 lines
40 KiB
JavaScript
Raw Permalink 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.
'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 = `<div class="empty">Apps konnten nicht geladen werden.</div>`; }
}
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='<div class="empty">Raumliste konnte nicht geladen werden.</div>';
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='<div class="empty">Verlauf konnte nicht geladen werden.</div>'; }
}
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='<div class="empty">Dateiliste konnte nicht geladen werden.</div>';}}
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*</g,'><').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;i<lines.length;i++){if(/^\t+/.test(lines[i])||/^ +\t/.test(lines[i]))throw new Error(`YAML: Tab-Einrückung in Zeile ${i+1}`);const raw=lines[i].trim();if(!raw||raw.startsWith('#'))continue;if(/^[-?:]\s*$/.test(raw))continue;const quotes=(raw.match(/"/g)||[]).length;if(quotes%2)throw new Error(`YAML: ungerade Anzahl doppelter Anführungszeichen in Zeile ${i+1}`);}if(action==='validate')return 'Basisprüfung erfolgreich. Hinweis: Dies ist keine vollständige YAML-Schema-/Semantikprüfung.';if(action==='minify')return lines.map(x=>x.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<A.length||j<B.length){if(i<A.length&&j<B.length&&A[i]===B[j]){out.push([' ',A[i]]);i++;j++;}else if(j<B.length&&(i===A.length||dp[i][j+1]>=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<cnt;n++){let x=0n;for(let i=0;i<4;i++)x|=BigInt(b[o++])<<BigInt(i*8);subs.push(x);}return `S-${rev}-${auth}-${subs.join('-')}`;}
$('#sidConvert').addEventListener('click',()=>{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;i<bytes.length;i+=step)s+=String.fromCharCode(...bytes.subarray(i,i+step));return btoa(s);}function base64ToBytes(s){const bin=atob(s.replace(/\s+/g,''));return Uint8Array.from(bin,c=>c.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<text.length;i++){const c=text[i];if(quoted){if(c==='"'&&text[i+1]==='"'){field+='"';i++;}else if(c==='"')quoted=false;else field+=c;}else if(c==='"')quoted=true;else if(c===delim){row.push(field);field='';}else if(c==='\n'){row.push(field.replace(/\r$/,''));rows.push(row);row=[];field='';}else field+=c;}if(field||row.length){row.push(field.replace(/\r$/,''));rows.push(row);}return rows;}
function renderCSV(){const q=$('#csvFilter').value.toLocaleLowerCase('de');const wrap=$('#csvTableWrap');wrap.replaceChildren();if(!csvRows.length)return;const head=csvRows[0];const data=csvRows.slice(1).filter(r=>!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;i<head.length;i++){const td=document.createElement('td');td.textContent=r[i]??'';tr.append(td)}body.append(tr)});table.append(body);wrap.append(table);$('#csvMeta').textContent=`${data.length} von ${Math.max(0,csvRows.length-1)} Datenzeilen · ${head.length} Spalten${data.length>1000?' · 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();