/* ===================================================================
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);
})();