431 lines
17 KiB
JavaScript
431 lines
17 KiB
JavaScript
/* ===================================================================
|
|
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':
|
|
`<?php
|
|
/**
|
|
* Basis-Konfiguration für WordPress — shop.bergmann-coffee.com
|
|
*/
|
|
|
|
// ** Datenbank-Einstellungen ** //
|
|
define( 'DB_NAME', 'bergmann_prod' );
|
|
define( 'DB_USER', 'bergmann_wp' );
|
|
define( 'DB_PASSWORD', getenv('WP_DB_PASS') );
|
|
define( 'DB_HOST', '127.0.0.1:3306' );
|
|
define( 'DB_CHARSET', 'utf8mb4' );
|
|
define( 'DB_COLLATE', '' );
|
|
|
|
$table_prefix = 'wp_bg_';
|
|
|
|
// ** Speicher & Performance ** //
|
|
define( 'WP_MEMORY_LIMIT', '256M' );
|
|
define( 'WP_MAX_MEMORY_LIMIT', '512M' );
|
|
define( 'AUTOSAVE_INTERVAL', 60 );
|
|
define( 'WP_POST_REVISIONS', 10 );
|
|
define( 'WP_CACHE', true );
|
|
|
|
// ** Sicherheit ** //
|
|
define( 'DISALLOW_FILE_EDIT', true );
|
|
define( 'FORCE_SSL_ADMIN', true );
|
|
define( 'AUTOMATIC_UPDATER_DISABLED', false );
|
|
|
|
// ** Debug ** //
|
|
define( 'WP_DEBUG', false );
|
|
define( 'WP_DEBUG_LOG', '/var/www/bergmann/logs/wp-debug.log' );
|
|
define( 'WP_DEBUG_DISPLAY', false );
|
|
|
|
// ** Authentifizierungs-Schlüssel (rotiert 14.03.2026) ** //
|
|
define( 'AUTH_KEY', getenv('WP_AUTH_KEY') );
|
|
define( 'SECURE_AUTH_KEY', getenv('WP_SECURE_AUTH_KEY') );
|
|
define( 'LOGGED_IN_KEY', getenv('WP_LOGGED_IN_KEY') );
|
|
define( 'NONCE_KEY', getenv('WP_NONCE_KEY') );
|
|
|
|
if ( ! defined( 'ABSPATH' ) ) define( 'ABSPATH', __DIR__ . '/' );
|
|
require_once ABSPATH . 'wp-settings.php';
|
|
`,
|
|
'htdocs/.htaccess':
|
|
`# BEGIN WordPress
|
|
<IfModule mod_rewrite.c>
|
|
RewriteEngine On
|
|
RewriteBase /
|
|
RewriteRule ^index\\.php$ - [L]
|
|
RewriteCond %{REQUEST_FILENAME} !-f
|
|
RewriteCond %{REQUEST_FILENAME} !-d
|
|
RewriteRule . /index.php [L]
|
|
</IfModule>
|
|
# END WordPress
|
|
|
|
# HTTPS erzwingen
|
|
RewriteCond %{HTTPS} off
|
|
RewriteRule ^(.*)$ https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301]
|
|
|
|
# Sicherheits-Header
|
|
<IfModule mod_headers.c>
|
|
Header set X-Content-Type-Options "nosniff"
|
|
Header set X-Frame-Options "SAMEORIGIN"
|
|
Header set Referrer-Policy "strict-origin-when-cross-origin"
|
|
</IfModule>
|
|
|
|
# Browser-Caching
|
|
<IfModule mod_expires.c>
|
|
ExpiresActive On
|
|
ExpiresByType image/webp "access plus 1 year"
|
|
ExpiresByType text/css "access plus 1 month"
|
|
</IfModule>
|
|
`,
|
|
'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':
|
|
`<?php
|
|
/**
|
|
* Front-Controller. Lädt die WordPress-Umgebung und das Template.
|
|
*/
|
|
define( 'WP_USE_THEMES', true );
|
|
require __DIR__ . '/wp-blog-header.php';
|
|
`,
|
|
'htdocs/wp-content/themes/storefront-child/functions.php':
|
|
`<?php
|
|
/**
|
|
* Storefront Child — Funktionen
|
|
*/
|
|
add_action( 'wp_enqueue_scripts', function () {
|
|
wp_enqueue_style( 'storefront-parent', get_template_directory_uri() . '/style.css' );
|
|
wp_enqueue_style( 'storefront-child', get_stylesheet_uri(), array('storefront-parent'), '1.4.2' );
|
|
}, 20 );
|
|
|
|
// WooCommerce: 4 Produkte pro Reihe
|
|
add_filter( 'loop_shop_columns', fn() => 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:'<svg class="ti-ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9"><path d="M3 7a2 2 0 0 1 2-2h4l2 2h8a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/></svg>',
|
|
file:'<svg class="ti-ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><path d="M14 2v6h6"/></svg>',
|
|
chev:'<svg class="chev" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M9 6l6 6-6 6"/></svg>',
|
|
};
|
|
|
|
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 + '<span>'+node.name+'</span>';
|
|
}else{
|
|
row.innerHTML = '<span style="width:13px;flex:none"></span>' + ICO.file + '<span>'+node.name+'</span>';
|
|
}
|
|
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 = '<span style="width:13px"></span><span>leer</span>';
|
|
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 ? '<b>'+p+'</b>' : 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 += '<div>'+i+'</div>';
|
|
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 = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="5" cy="12" r="1.5"/><circle cx="12" cy="12" r="1.5"/><circle cx="19" cy="12" r="1.5"/></svg>';
|
|
const runIco = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M6 4l14 8-14 8z"/></svg>';
|
|
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',`
|
|
<tr>
|
|
<td><div class="site-name" style="max-width:220px">${c.name}</div></td>
|
|
<td><span class="pill accent">${c.expr}</span></td>
|
|
<td><span class="site-meta" style="max-width:280px;display:inline-block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;vertical-align:bottom">${c.cmd}</span></td>
|
|
<td><span class="site-meta">${c.last}</span></td>
|
|
<td><span class="health"><span class="health-dot ${st}"></span><span class="health-label">${lbl}</span></span></td>
|
|
<td class="num"><div class="row-actions"><button title="Jetzt ausführen">${runIco}</button><button title="Mehr">${moreIco}</button></div></td>
|
|
</tr>`);
|
|
});
|
|
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 `<div class="log-line"><span class="log-time">${t}</span><span class="log-lvl lvl-${lvl}">${lvl}</span><span class="log-msg">${msg}</span></div>`;
|
|
}
|
|
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);
|
|
|
|
})();
|