/* =================================================================== CluPilot — Site Details (Datei-Manager, Editor, Cronjobs, Logs) =================================================================== */ (function(){ 'use strict'; /* ---------- generic toggles ---------- */ document.addEventListener('click', e=>{ const t = e.target.closest('.toggle'); if(t && !t.closest('[data-toggle]')) t.classList.toggle('on'); }); /* ---------- detail tab switching ---------- */ const tabs = document.querySelectorAll('#dtabs .dtab'); const panes = { overview:'tab-overview', wp:'tab-wp', php:'tab-php', files:'tab-files', cron:'tab-cron', logs:'tab-logs' }; tabs.forEach(tab=>tab.addEventListener('click',()=>{ tabs.forEach(t=>t.classList.remove('active')); tab.classList.add('active'); Object.values(panes).forEach(id=>document.getElementById(id).classList.remove('active')); document.getElementById(panes[tab.dataset.tab]).classList.add('active'); if(tab.dataset.tab==='logs') renderLog(currentLog); })); /* ===================================================================== FILE MANAGER + EDITOR ===================================================================== */ const fileContents = { 'htdocs/wp-config.php': ` RewriteEngine On RewriteBase / RewriteRule ^index\\.php$ - [L] RewriteCond %{REQUEST_FILENAME} !-f RewriteCond %{REQUEST_FILENAME} !-d RewriteRule . /index.php [L] # END WordPress # HTTPS erzwingen RewriteCond %{HTTPS} off RewriteRule ^(.*)$ https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301] # Sicherheits-Header Header set X-Content-Type-Options "nosniff" Header set X-Frame-Options "SAMEORIGIN" Header set Referrer-Policy "strict-origin-when-cross-origin" # Browser-Caching ExpiresActive On ExpiresByType image/webp "access plus 1 year" ExpiresByType text/css "access plus 1 month" `, 'htdocs/robots.txt': `User-agent: * Disallow: /wp-admin/ Allow: /wp-admin/admin-ajax.php Disallow: /cart/ Disallow: /checkout/ Disallow: /my-account/ Sitemap: https://shop.bergmann-coffee.com/sitemap_index.xml `, 'htdocs/index.php': ` 4 ); // Checkout: Telefon optional add_filter( 'woocommerce_checkout_fields', function ( $fields ) { $fields['billing']['billing_phone']['required'] = false; return $fields; }); // Heartbeat im Admin drosseln add_filter( 'heartbeat_settings', function ( $s ) { $s['interval'] = 60; return $s; }); `, 'htdocs/wp-content/themes/storefront-child/style.css': `/* Theme Name: Storefront Child Template: storefront Version: 1.4.2 */ :root{ --bg-accent:#6f4e37; --bg-cream:#f5efe6; } .site-header{ background:var(--bg-cream); border-bottom:3px solid var(--bg-accent); } .button.alt{ background:var(--bg-accent); } .price{ font-family:"JetBrains Mono", monospace; font-weight:600; } `, 'php.ini': `; php.ini — pool: bergmann (php 8.1-fpm) memory_limit = 256M max_execution_time = 120 max_input_time = 120 max_input_vars = 3000 upload_max_filesize = 128M post_max_size = 128M default_charset = "UTF-8" date.timezone = Europe/Berlin display_errors = Off log_errors = On error_log = /var/www/bergmann/logs/php-error.log allow_url_fopen = On [opcache] opcache.enable = 1 opcache.memory_consumption = 256 opcache.max_accelerated_files = 10000 opcache.revalidate_freq = 60 `, 'logs/access.log': `192.0.2.41 - - [31/May/2026:09:12:04] "GET /shop/ HTTP/2.0" 200 18421 203.0.113.7 - - [31/May/2026:09:12:05] "GET /wp-json/wc/store/cart HTTP/2.0" 200 842 192.0.2.41 - - [31/May/2026:09:12:09] "POST /?wc-ajax=add_to_cart HTTP/2.0" 200 311 `, }; const tree = [ {name:'htdocs', type:'dir', open:true, children:[ {name:'wp-admin', type:'dir', children:[]}, {name:'wp-content', type:'dir', open:true, children:[ {name:'themes', type:'dir', open:true, children:[ {name:'storefront-child', type:'dir', open:true, children:[ {name:'functions.php', type:'file', lang:'PHP'}, {name:'style.css', type:'file', lang:'CSS'}, ]}, ]}, {name:'plugins', type:'dir', children:[]}, {name:'uploads', type:'dir', children:[]}, ]}, {name:'wp-includes', type:'dir', children:[]}, {name:'.htaccess', type:'file', lang:'APACHE'}, {name:'index.php', type:'file', lang:'PHP'}, {name:'robots.txt', type:'file', lang:'TEXT'}, {name:'wp-config.php', type:'file', lang:'PHP'}, ]}, {name:'php.ini', type:'file', lang:'INI'}, {name:'logs', type:'dir', open:true, children:[ {name:'access.log', type:'file', lang:'LOG'}, {name:'error.log', type:'file', lang:'LOG'}, ]}, ]; const ICO = { dirClosed:'', file:'', chev:'', }; const treeEl = document.getElementById('file-tree'); function renderTree(nodes, parentPath, container){ nodes.forEach(node=>{ const path = parentPath ? parentPath+'/'+node.name : node.name; const row = document.createElement('div'); row.className = 'tree-item '+(node.type==='dir'?'dir':'') + (node.open?' open':''); if(node.type==='dir'){ row.innerHTML = ICO.chev + ICO.dirClosed + ''+node.name+''; }else{ row.innerHTML = '' + ICO.file + ''+node.name+''; } container.appendChild(row); if(node.type==='dir'){ const kids = document.createElement('div'); kids.className = 'tree-children'+(node.open?'':' collapsed'); container.appendChild(kids); if(node.children && node.children.length){ renderTree(node.children, path, kids); }else{ const empty = document.createElement('div'); empty.className = 'tree-item'; empty.style.cssText = 'color:var(--muted-2);font-style:italic;cursor:default'; empty.innerHTML = 'leer'; kids.appendChild(empty); } row.addEventListener('click',()=>{ row.classList.toggle('open'); kids.classList.toggle('collapsed'); }); }else{ row.addEventListener('click',()=>openFile(path, node.lang, row)); } }); } renderTree(tree, '', treeEl); /* ---- editor ---- */ const codeWrap = document.getElementById('code-wrap'); const editorEmpty = document.getElementById('editor-empty'); const gutter = document.getElementById('gutter'); const codeArea = document.getElementById('code-area'); const crumb = document.getElementById('crumb'); const tagline = document.getElementById('editor-tagline'); const status = document.getElementById('editor-status'); let currentPath = null, original = ''; function openFile(path, lang, row){ document.querySelectorAll('.tree-item').forEach(r=>r.classList.remove('active')); if(row) row.classList.add('active'); const content = fileContents[path] !== undefined ? fileContents[path] : '# '+path+'\n# (Inhalt wird vom Server geladen…)\n'; currentPath = path; original = content; codeArea.value = content; editorEmpty.style.display = 'none'; codeWrap.style.display = 'flex'; const parts = path.split('/'); crumb.innerHTML = parts.map((p,i)=> i===parts.length-1 ? ''+p+'' : p).join(' / '); tagline.textContent = lang || ''; status.textContent = 'bereit'; buildGutter(); } function buildGutter(){ const lines = codeArea.value.split('\n').length; let html = ''; for(let i=1;i<=lines;i++) html += '
'+i+'
'; gutter.innerHTML = html; gutter.scrollTop = codeArea.scrollTop; } codeArea.addEventListener('input',()=>{ buildGutter(); status.textContent='● ungespeichert'; status.style.color='var(--warning)'; }); codeArea.addEventListener('scroll',()=>{ gutter.scrollTop = codeArea.scrollTop; }); document.getElementById('btn-save').addEventListener('click',()=>{ if(!currentPath) return; fileContents[currentPath] = codeArea.value; original = codeArea.value; status.textContent='✓ gespeichert'; status.style.color='var(--success)'; }); document.getElementById('btn-revert').addEventListener('click',()=>{ codeArea.value = original; buildGutter(); status.textContent='zurückgesetzt'; status.style.color='var(--muted)'; }); /* open wp-config.php by default */ (function(){ const rows = [...document.querySelectorAll('.tree-item')]; const cfg = rows.find(r=>r.textContent.trim()==='wp-config.php'); if(cfg) openFile('htdocs/wp-config.php','PHP',cfg); })(); /* ===================================================================== CRONJOBS ===================================================================== */ const cronBody = document.getElementById('cron-body'); const cronCount = document.getElementById('cron-count'); let crons = [ {name:'WP-Cron Hauptlauf', expr:'*/5 * * * *', cmd:'wp cron event run --due-now', last:'vor 2 min', st:'ok'}, {name:'WooCommerce Lagerabgleich', expr:'0 * * * *', cmd:'wp wc tool run regenerate_stock', last:'vor 41 min', st:'ok'}, {name:'Tägliches Backup', expr:'0 3 * * *', cmd:'clupilot backup create --site=bergmann', last:'heute 03:00', st:'ok'}, {name:'Sitemap neu aufbauen', expr:'0 4 * * *', cmd:'wp yoast index --reindex', last:'heute 04:00', st:'ok'}, {name:'Transients aufräumen', expr:'0 2 * * 0', cmd:'wp transient delete --expired', last:'vor 3 tagen', st:'warn'}, ]; const moreIco = ''; const runIco = ''; function renderCron(){ cronBody.innerHTML = ''; crons.forEach(c=>{ const st = c.st==='ok'?'ok':c.st==='bad'?'bad':'warn'; const lbl = c.st==='ok'?'aktiv':c.st==='bad'?'fehler':'übersprungen'; cronBody.insertAdjacentHTML('beforeend',`
${c.name}
${c.expr} ${c.cmd} ${c.last} ${lbl}
`); }); cronCount.textContent = crons.length; document.querySelector('.dtab[data-tab="cron"] .count').textContent = crons.length; } renderCron(); const cronForm = document.getElementById('cron-form'); document.getElementById('btn-add-cron').addEventListener('click',()=>{ cronForm.style.display = cronForm.style.display==='none'?'':'none'; }); document.getElementById('cf-cancel').addEventListener('click',()=>{ cronForm.style.display='none'; }); document.getElementById('cf-preset').addEventListener('change',e=>{ const v = e.target.value; if(v!=='custom') document.getElementById('cf-expr').value = v; }); document.getElementById('cf-save').addEventListener('click',()=>{ const name = document.getElementById('cf-name').value.trim() || 'Neuer Cronjob'; const expr = document.getElementById('cf-expr').value.trim() || '0 3 * * *'; const cmd = document.getElementById('cf-cmd').value.trim() || '—'; crons.unshift({name, expr, cmd, last:'noch nicht gelaufen', st:'warn'}); renderCron(); cronForm.style.display='none'; document.getElementById('cf-name').value=''; }); /* ===================================================================== LOGS ===================================================================== */ const logData = { access:[ ['09:12:04','INFO','192.0.2.41 "GET /shop/" 200 18421 — 142ms'], ['09:12:05','INFO','203.0.113.7 "GET /wp-json/wc/store/cart" 200 842 — 38ms'], ['09:12:09','INFO','192.0.2.41 "POST /?wc-ajax=add_to_cart" 200 311 — 96ms'], ['09:12:14','INFO','198.51.100.2 "GET /produkt/espresso-blend/" 200 21044 — 158ms'], ['09:12:21','WARN','45.155.* "GET /wp-login.php" 403 — rate-limit'], ['09:12:33','INFO','192.0.2.88 "GET /warenkorb/" 200 9920 — 71ms'], ['09:12:40','INFO','Googlebot "GET /sitemap_index.xml" 200 4210 — 22ms'], ['09:12:48','INFO','192.0.2.41 "POST /checkout/" 302 0 — 210ms'], ], error:[ ['09:05:11','WARN','PHP Notice: Undefined index "shipping_zone" in cart.php:88'], ['09:07:42','ERROR','Uncaught TypeError: count(): Argument must be array — class-wc-cart.php:412'], ['09:09:03','INFO','Cache geleert: object-cache (redis) — 18421 keys'], ['08:58:19','WARN','Slow query (2,1s): wp_bg_postmeta — SELECT meta_value …'], ], php:[ ['09:00:01','OK','php-fpm pool "bergmann" reloaded — 8 worker'], ['09:03:55','INFO','OPcache: 4218 scripts cached, 99.2% hit rate'], ['09:08:12','WARN','Worker 3: memory peak 214M / 256M'], ['09:11:40','INFO','request /checkout/ — 210ms, mem 41M'], ], wp:[ ['09:01:00','DEBUG','WC_Cart::calculate_totals() — 12 items'], ['09:04:22','DEBUG','do_action("woocommerce_payment_complete", 48211)'], ['09:06:30','WARN','Plugin "Smart Coupons" deprecated hook: woocommerce_add_to_cart'], ['09:10:05','DEBUG','wp_mail() → kunde@example.com — Bestellbestätigung #48211'], ], }; const consoleEl = document.getElementById('console'); let currentLog = 'access'; function line(t,lvl,msg){ return `
${t}${lvl}${msg}
`; } function renderLog(type){ currentLog = type; consoleEl.innerHTML = logData[type].map(l=>line(l[0],l[1],l[2])).join(''); consoleEl.scrollTop = consoleEl.scrollHeight; document.getElementById('log-title').textContent = ({access:'access.log',error:'error.log',php:'php-fpm',wp:'wp-debug'})[type]; } document.querySelectorAll('#log-seg button').forEach(b=>b.addEventListener('click',()=>{ document.querySelectorAll('#log-seg button').forEach(x=>x.classList.remove('active')); b.classList.add('active'); renderLog(b.dataset.log); })); document.getElementById('log-clear').addEventListener('click',()=>{ logData[currentLog]=[]; renderLog(currentLog); }); const followBtn = document.getElementById('log-follow'); followBtn.addEventListener('click',()=>followBtn.classList.toggle('on')); const sampleMsgs = { access:['INFO','"GET /shop/" 200 — 130ms'], error:['INFO','Cache hit ratio 99.3%'], php:['INFO','request handled — 88ms'], wp:['DEBUG','transient set: wc_products_onsale'], }; function tick(){ if(followBtn.classList.contains('on') && document.getElementById('tab-logs').classList.contains('active')){ const now = new Date(); const t = String(now.getHours()).padStart(2,'0')+':'+String(now.getMinutes()).padStart(2,'0')+':'+String(now.getSeconds()).padStart(2,'0'); const s = sampleMsgs[currentLog]; const ip = currentLog==='access' ? '192.0.2.'+(10+Math.floor(Math.random()*240))+' ' : ''; logData[currentLog].push([t, s[0], ip+s[1]]); if(logData[currentLog].length>200) logData[currentLog].shift(); consoleEl.insertAdjacentHTML('beforeend', line(t,s[0],ip+s[1])); consoleEl.scrollTop = consoleEl.scrollHeight; } } setInterval(tick, 2600); })();