clupilot/templates/akutell/sitedetails.js

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