clupilot/app/resources/views/livewire/pages/sites/show.blade.php

584 lines
36 KiB
PHP

<?php
use App\Models\Site;
use Livewire\Attributes\Layout;
use Livewire\Attributes\Url;
use Livewire\Volt\Component;
new #[Layout('layouts.app')] class extends Component {
public Site $site;
#[Url(as: 'tab')]
public string $tab = 'overview';
public string $activeFile = 'htdocs/wp-config.php';
public bool $logFollow = true;
public string $currentLog = 'access';
public function mount(Site $site): void
{
$this->site = $site->load(['server', 'certificate', 'crons']);
}
public function setTab(string $t): void { $this->tab = $t; }
public function setActiveFile(string $path): void { $this->activeFile = $path; }
public function setLog(string $type): void { $this->currentLog = $type; }
public function toggleMaintenance(): void
{
$this->site->maintenance_mode = ! $this->site->maintenance_mode;
$this->site->save();
}
public function toggleCache(): void { $this->site->cache_enabled = ! $this->site->cache_enabled; $this->site->save(); }
public function toggleCdn(): void { $this->site->cdn_enabled = ! $this->site->cdn_enabled; $this->site->save(); }
/* File tree (nested) — verbatim from templates/akutell/sitedetails.js */
public function getFileTreeProperty(): array
{
return [
[
'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', 'path' => 'htdocs/wp-content/themes/storefront-child/functions.php'],
['name' => 'style.css', 'type' => 'file', 'lang' => 'CSS', 'path' => 'htdocs/wp-content/themes/storefront-child/style.css'],
],
],
],
],
['name' => 'plugins', 'type' => 'dir', 'children' => []],
['name' => 'uploads', 'type' => 'dir', 'children' => []],
],
],
['name' => 'wp-includes', 'type' => 'dir', 'children' => []],
['name' => '.htaccess', 'type' => 'file', 'lang' => 'APACHE', 'path' => 'htdocs/.htaccess'],
['name' => 'index.php', 'type' => 'file', 'lang' => 'PHP', 'path' => 'htdocs/index.php'],
['name' => 'robots.txt', 'type' => 'file', 'lang' => 'TEXT', 'path' => 'htdocs/robots.txt'],
['name' => 'wp-config.php','type' => 'file', 'lang' => 'PHP', 'path' => 'htdocs/wp-config.php'],
],
],
['name' => 'php.ini', 'type' => 'file', 'lang' => 'INI', 'path' => 'php.ini'],
[
'name' => 'logs', 'type' => 'dir', 'open' => true,
'children' => [
['name' => 'access.log', 'type' => 'file', 'lang' => 'LOG', 'path' => 'logs/access.log'],
['name' => 'error.log', 'type' => 'file', 'lang' => 'LOG', 'path' => 'logs/error.log'],
],
],
];
}
/* fileContents from sitedetails.js verbatim */
public function getFileContentsProperty(): array
{
return [
'htdocs/wp-config.php' => <<<'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 );
if ( ! defined( 'ABSPATH' ) ) define( 'ABSPATH', __DIR__ . '/' );
require_once ABSPATH . 'wp-settings.php';
PHP,
'htdocs/.htaccess' => <<<'TXT'
# 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
<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>
TXT,
'htdocs/robots.txt' => <<<'TXT'
User-agent: *
Disallow: /wp-admin/
Disallow: /warenkorb/
Disallow: /checkout/
Allow: /wp-admin/admin-ajax.php
Sitemap: https://shop.bergmann-coffee.com/sitemap_index.xml
TXT,
'htdocs/index.php' => <<<'PHP'
<?php
/** Sets up the WordPress Environment. */
define( 'WP_USE_THEMES', true );
require __DIR__ . '/wp-blog-header.php';
PHP,
'htdocs/wp-content/themes/storefront-child/functions.php' => <<<'PHP'
<?php
// Storefront-Child Funktionen — bergmann-coffee
add_action('wp_enqueue_scripts', function () {
wp_enqueue_style('storefront-style', get_template_directory_uri() . '/style.css');
wp_enqueue_style('storefront-child', get_stylesheet_uri(), ['storefront-style']);
});
add_filter('manage_edit-shop_order_columns', function ($cols) {
$cols['region'] = 'Region';
return $cols;
});
add_filter('woocommerce_checkout_fields', function ($fields) {
unset($fields['billing']['billing_company']);
return $fields;
});
add_filter('heartbeat_settings', fn ($s) => array_merge($s, ['interval' => 60]));
PHP,
'htdocs/wp-content/themes/storefront-child/style.css' => <<<'CSS'
/*
Theme Name: Storefront Child — Bergmann
Template: storefront
*/
:root { --brand: #15a06a; --brand-dark: #0d6c4a; }
.button.alt { background: var(--brand); border-color: var(--brand); }
.button.alt:hover { background: var(--brand-dark); }
CSS,
'php.ini' => <<<'INI'
; bergmann-coffee · php-fpm pool
memory_limit = 256M
max_execution_time = 60
max_input_time = 60
upload_max_filesize = 64M
post_max_size = 64M
date.timezone = Europe/Berlin
opcache.enable = 1
opcache.memory_consumption = 256
opcache.max_accelerated_files = 20000
opcache.validate_timestamps = 0
INI,
'logs/access.log' => <<<'LOG'
192.0.2.41 - - [12/Mai/2026:09:12:04 +0200] "GET /shop/ HTTP/2.0" 200 18421
203.0.113.7 - - [12/Mai/2026:09:12:05 +0200] "GET /wp-json/wc/store/cart HTTP/2.0" 200 842
192.0.2.41 - - [12/Mai/2026:09:12:09 +0200] "POST /?wc-ajax=add_to_cart HTTP/2.0" 200 311
LOG,
'logs/error.log' => "[error] PHP Notice: Undefined index 'shipping_zone' in cart.php:88\n[error] Uncaught TypeError: count(): Argument must be array in class-wc-cart.php:412\n",
];
}
/* Log entries per type (xterm renders these client-side) */
public function getLogDataProperty(): array
{
return [
'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'],
],
];
}
public function getCurrentFileContentProperty(): string
{
return $this->fileContents[$this->activeFile] ?? '';
}
}; ?>
@php
/** Recursive file tree renderer. */
if (! function_exists('clu_render_tree')) {
function clu_render_tree(array $items, string $activeFile): string {
$out = '';
foreach ($items as $node) {
$isDir = ($node['type'] ?? '') === 'dir';
$isOpen = $node['open'] ?? false;
$isActive = ! $isDir && ($node['path'] ?? null) === $activeFile;
$rowClass = 'clu-tree-item' . ($isDir ? ' dir' : '') . ($isOpen ? ' open' : '') . ($isActive ? ' active' : '');
$click = $isDir ? '' : 'wire:click="setActiveFile(\''.addslashes($node['path']).'\')" wire:navigate.hover';
$ico = $isDir
? '<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>'
: '<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 = $isDir
? '<svg class="clu-chev" width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M9 6l6 6-6 6"/></svg>'
: '<span style="width:11px;flex:none;"></span>';
$out .= '<div class="'.$rowClass.'" '.$click.'>'.$chev.$ico.'<span>'.htmlspecialchars($node['name']).'</span></div>';
if ($isDir && ! empty($node['children'])) {
$hidden = $isOpen ? '' : ' collapsed';
$out .= '<div class="clu-tree-children'.$hidden.'">'.clu_render_tree($node['children'], $activeFile).'</div>';
}
}
return $out;
}
}
@endphp
<div>
{{-- ── Topbar ── --}}
<div class="clu-topbar">
<x-clu.burger />
<a href="{{ route('sites.index') }}" wire:navigate class="clu-icon-btn" aria-label="Zurück">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M15 18l-6-6 6-6"/></svg>
</a>
<div class="clu-site-hero">
<div class="clu-favicon" style="background:#15a06a;">{{ mb_strtoupper(mb_substr($site->domain, 0, 1)) }}</div>
<div>
<h1>{{ $site->domain }}</h1>
<div class="sub">
<span>{{ $site->server->name }}</span>
<span>· wp {{ $site->wp_version }}</span>
<span>· php {{ $site->php_version }}</span>
<span>· ssl ✓</span>
</div>
</div>
</div>
<span class="clu-status-chip"><span class="clu-pulse-dot"></span> online</span>
<div class="spacer"></div>
<a href="https://{{ $site->domain }}" target="_blank" rel="noopener" class="clu-ghost-btn">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14 3h7v7M21 3l-9 9M19 14v5a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V7a2 2 0 0 1 2-2h5"/></svg>
Besuchen
</a>
<button class="clu-btn-primary" style="height:36px;padding:0 14px;" type="button">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M4 17l5-5-5-5M13 19h7"/></svg>
WP-Admin
</button>
</div>
{{-- ── Tab bar ── --}}
<div class="clu-dtabs mb-[18px]" role="tablist">
@foreach (['overview' => ['Übersicht', '<rect x="3" y="3" width="7" height="7" rx="1.5"/><rect x="14" y="3" width="7" height="7" rx="1.5"/><rect x="3" y="14" width="7" height="7" rx="1.5"/><rect x="14" y="14" width="7" height="7" rx="1.5"/>'], 'wp' => ['WordPress', '<circle cx="12" cy="12" r="9"/><path d="M3 12h18M12 3a14 14 0 0 1 0 18M12 3a14 14 0 0 0 0 18"/>'], 'php' => ['PHP / ini', '<path d="M4 7h16M4 12h16M4 17h10"/>'], 'files' => ['Datei-Manager', '<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"/>'], 'cron' => ['Cronjobs', '<circle cx="12" cy="12" r="9"/><path d="M12 7v5l3 2"/>'], 'logs' => ['Logs', '<rect x="3" y="3" width="18" height="18" rx="2"/><path d="M7 8h10M7 12h10M7 16h6"/>']] as $key => [$label, $svg])
<button type="button" role="tab"
wire:click="setTab('{{ $key }}')"
class="clu-dtab @if($tab === $key) active @endif">
<svg class="ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9">{!! $svg !!}</svg>
{{ $label }}
@if ($key === 'cron' && $site->crons->count())
<span class="count">{{ $site->crons->count() }}</span>
@endif
</button>
@endforeach
</div>
{{-- ────── OVERVIEW ────── --}}
@if ($tab === 'overview')
<div class="clu-pane">
<div class="grid gap-[14px]" style="grid-template-columns:repeat(auto-fit,minmax(220px,1fr));">
<div class="clu-metric"><div class="clu-metric-row"><span class="clu-metric-label">Besucher / 24h</span></div><div class="clu-metric-value">{{ number_format($site->visitors_24h, 0, ',', '.') }}</div><div class="clu-metric-delta">↑ 9.2% vs. gestern</div></div>
<div class="clu-metric"><div class="clu-metric-row"><span class="clu-metric-label">Speicher</span></div><div class="clu-metric-value">14.2<span class="unit">/40 GB</span></div><div class="clu-metric-delta">36% genutzt</div></div>
<div class="clu-metric success"><div class="clu-metric-row"><span class="clu-metric-label">PageSpeed</span></div><div class="clu-metric-value">{{ $site->pagespeed_score }}<span class="unit">/100</span></div><div class="clu-metric-delta">Core Web Vitals OK</div></div>
<div class="clu-metric @if($site->updates_pending > 0) warning @endif"><div class="clu-metric-row"><span class="clu-metric-label">Updates</span></div><div class="clu-metric-value">{{ $site->updates_pending }}</div><div class="clu-metric-delta @if($site->updates_pending > 0) warn @endif">{{ $site->updates_pending > 0 ? 'verfügbar' : 'aktuell' }}</div></div>
</div>
<div class="grid gap-[14px]" style="grid-template-columns:1fr 1fr;">
<div class="clu-card">
<div class="clu-card-head"><h3>Installation</h3></div>
<div class="clu-card-body" style="display:flex;flex-direction:column;gap:9px;">
<div class="flex justify-between"><span class="text-(--color-muted)">Domain</span><span class="font-mono">{{ $site->domain }}</span></div>
<div class="flex justify-between"><span class="text-(--color-muted)">Server</span><span class="font-mono">{{ $site->server->name }} · {{ $site->server->region }}</span></div>
<div class="flex justify-between"><span class="text-(--color-muted)">WordPress</span><span class="clu-pill @if($site->updates_pending > 0) warning @else success @endif">{{ $site->wp_version }}</span></div>
<div class="flex justify-between"><span class="text-(--color-muted)">PHP</span><span class="clu-pill muted">{{ $site->php_version }}</span></div>
<div class="flex justify-between"><span class="text-(--color-muted)">SSL</span><span class="clu-pill success">Let's Encrypt</span></div>
<div class="flex justify-between"><span class="text-(--color-muted)">DB-Größe</span><span class="font-mono">412 MB</span></div>
<div class="flex justify-between"><span class="text-(--color-muted)">Letztes Deploy</span><span class="font-mono">vor 4h</span></div>
</div>
</div>
<div class="clu-card">
<div class="clu-card-head"><h3>Schnellaktionen</h3></div>
<div class="clu-card-body" style="display:flex;flex-direction:column;gap:12px;">
<label class="clu-checkbox" style="justify-content:space-between;width:100%;">
<span>Maintenance Mode</span>
<input type="checkbox" wire:click="toggleMaintenance" @checked($site->maintenance_mode) />
<span class="box"><svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3.5"><path d="M5 12l5 5L20 7"/></svg></span>
</label>
<label class="clu-checkbox" style="justify-content:space-between;width:100%;">
<span>Page Cache</span>
<input type="checkbox" wire:click="toggleCache" @checked($site->cache_enabled) />
<span class="box"><svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3.5"><path d="M5 12l5 5L20 7"/></svg></span>
</label>
<label class="clu-checkbox" style="justify-content:space-between;width:100%;">
<span>CDN</span>
<input type="checkbox" wire:click="toggleCdn" @checked($site->cdn_enabled) />
<span class="box"><svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3.5"><path d="M5 12l5 5L20 7"/></svg></span>
</label>
<label class="clu-checkbox" style="justify-content:space-between;width:100%;">
<span>HTTPS erzwingen</span>
<input type="checkbox" @checked($site->https_enabled) />
<span class="box"><svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3.5"><path d="M5 12l5 5L20 7"/></svg></span>
</label>
</div>
</div>
</div>
</div>
@endif
{{-- ────── FILES ────── --}}
@if ($tab === 'files')
<div class="clu-pane">
<div class="clu-section-head">
<h3>Datei-Manager</h3>
<div class="flex gap-[8px]">
<button class="clu-ghost-btn" type="button"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 5v14M5 12h14"/></svg>Neue Datei</button>
<button class="clu-ghost-btn" type="button"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 3v14M5 10l7-7 7 7"/></svg>Upload</button>
</div>
</div>
<div class="clu-fm">
<div class="clu-card clu-tree">
<div class="clu-tree-head">
<span class="path">/var/www/bergmann</span>
<button type="button" class="clu-icon-btn" aria-label="Refresh"><svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 12a9 9 0 1 0 9-9"/><path d="M3 4v5h5"/></svg></button>
</div>
{!! clu_render_tree($this->fileTree, $activeFile) !!}
</div>
<div class="clu-card clu-editor" wire:key="editor-{{ $activeFile }}">
<div class="clu-editor-head">
<div class="clu-crumb">
{{ implode(' / ', array_slice(explode('/', $activeFile), 0, -1)) }} / <b>{{ basename($activeFile) }}</b>
</div>
<span class="clu-editor-tagline">{{ strtoupper(pathinfo($activeFile, PATHINFO_EXTENSION)) ?: 'TEXT' }}</span>
</div>
<div class="clu-code-wrap">
@php $lines = max(1, substr_count($this->currentFileContent, "\n") + 1); @endphp
<div class="clu-gutter">@for ($i = 1; $i <= $lines; $i++)<div>{{ $i }}</div>@endfor</div>
<textarea class="clu-code-area" spellcheck="false" wire:ignore>{{ $this->currentFileContent }}</textarea>
</div>
<div class="clu-editor-foot">
<span class="text-[12px] font-mono text-(--color-muted)">✓ geladen · {{ $lines }} Zeilen</span>
<div class="flex gap-[8px]">
<button class="clu-ghost-btn" type="button">Verwerfen</button>
<button class="clu-btn-primary" type="button" style="height:36px;padding:0 16px;">Speichern</button>
</div>
</div>
</div>
</div>
</div>
@endif
{{-- ────── CRON ────── --}}
@if ($tab === 'cron')
<div class="clu-pane">
<div class="clu-section-head">
<h3>Cronjobs</h3>
<button class="clu-btn-primary" type="button" style="height:36px;padding:0 14px;">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="M12 5v14M5 12h14"/></svg>
Cronjob anlegen
</button>
</div>
<div class="clu-card">
<div class="clu-card-head"><h3>Geplante Aufgaben <span class="clu-pill muted">{{ $site->crons->count() }}</span></h3></div>
<div class="clu-card-body tight" style="overflow-x:auto;">
<table class="clu-table">
<thead><tr><th>Name</th><th>Zeitplan</th><th>Befehl</th><th>Letzter Lauf</th><th>Status</th><th></th></tr></thead>
<tbody>
@forelse ($site->crons as $c)
<tr>
<td class="font-semibold">{{ $c->name }}</td>
<td><span class="clu-pill accent">{{ $c->expression }}</span></td>
<td class="font-mono text-[11.5px] text-(--color-muted)" style="max-width:280px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">{{ $c->command }}</td>
<td class="font-mono">{{ $c->last_run_at?->diffForHumans() ?? '—' }}</td>
<td>
@if ($c->status === 'ok')<span class="clu-health">ok</span>
@elseif ($c->status === 'failing')<span class="clu-health bad">failing</span>
@else<span class="clu-health warn">paused</span>@endif
</td>
<td><div class="clu-row-actions">
<button type="button" title="Run"><svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polygon points="6 4 20 12 6 20 6 4"/></svg></button>
<button type="button" title="Mehr"><svg width="13" height="13" 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></button>
</div></td>
</tr>
@empty
<tr><td colspan="6" style="text-align:center;padding:30px;color:var(--color-muted);">Noch keine Cronjobs.</td></tr>
@endforelse
</tbody>
</table>
</div>
</div>
</div>
@endif
{{-- ────── LOGS (xterm.js) ────── --}}
@if ($tab === 'logs')
<div class="clu-pane">
<div class="clu-section-head">
<h3>Logs · <span class="text-(--color-muted) text-[12px] font-mono">{{ $currentLog }}.log</span></h3>
<div class="flex items-center gap-[12px]">
<div class="clu-seg">
@foreach (['access', 'error', 'php', 'wp'] as $log)
<button type="button" wire:click="setLog('{{ $log }}')" class="@if($currentLog === $log) active @endif">{{ $log }}</button>
@endforeach
</div>
<label class="clu-switch-line">
<input type="checkbox" wire:model.live="logFollow" />
<span>Live folgen</span>
</label>
</div>
</div>
<div wire:ignore wire:key="xterm-{{ $currentLog }}">
<div class="clu-xterm-host" id="clu-xterm-host" data-log-type="{{ $currentLog }}"></div>
</div>
@push('scripts')
<script>
(function () {
const logData = @json($this->logData);
function initXterm() {
const host = document.getElementById('clu-xterm-host');
if (!host) return;
if (!window.XTerm) { setTimeout(initXterm, 30); return; }
if (host._term) host._term.dispose();
const term = new window.XTerm({
convertEol: true,
cursorBlink: false,
disableStdin: true,
fontFamily: 'JetBrains Mono, ui-monospace, monospace',
fontSize: 12.5,
lineHeight: 1.4,
theme: {
background: 'rgba(0,0,0,0)',
foreground: '#cfd3e6',
cursor: '#5aa9ff',
selectionBackground: 'rgba(99,140,255,0.25)',
},
});
const fit = new window.XTermFit();
term.loadAddon(fit);
term.open(host);
try { fit.fit(); } catch (e) {}
const COLOR = { INFO: '\x1b[38;2;90;169;255m', WARN: '\x1b[38;2;231;185;78m', ERROR: '\x1b[38;2;255;115;115m', OK: '\x1b[38;2;76;214;160m', DEBUG: '\x1b[38;2;169;139;255m', TIME: '\x1b[38;2;107;113;145m', RESET: '\x1b[0m' };
const lines = logData[host.dataset.logType] || [];
lines.forEach(([t, lvl, msg]) => {
term.writeln(`${COLOR.TIME}${t}${COLOR.RESET} ${COLOR[lvl] || ''}${lvl.padEnd(5)}${COLOR.RESET} ${msg}`);
});
host._term = term;
window.addEventListener('resize', () => { try { fit.fit(); } catch (e) {} });
}
initXterm();
document.addEventListener('livewire:navigated', initXterm);
})();
</script>
@endpush
</div>
@endif
{{-- ────── WP + PHP (kompakte Hardcoded-Konfig nach Template) ────── --}}
@if ($tab === 'wp')
<div class="clu-pane">
<div class="clu-card">
<div class="clu-card-head"><h3>wp-config.php</h3><span class="meta">WordPress core</span></div>
<div class="clu-card-body" style="display:flex;flex-direction:column;gap:14px;">
<div class="clu-set-grid" style="row-gap:14px;">
@foreach (['Tabellen-Präfix' => 'wp_bg_', 'WP_MEMORY_LIMIT' => '256M', 'AUTOSAVE_INTERVAL' => '60', 'WP_POST_REVISIONS' => '10', 'WP_DEBUG_LOG' => '/var/www/bergmann/logs/wp-debug.log', 'WP_HOME' => 'https://shop.bergmann-coffee.com'] as $label => $val)
<div class="flex flex-col gap-[6px]"><label class="text-[11.5px] text-(--color-muted) font-semibold">{{ $label }}</label><div class="clu-input"><input value="{{ $val }}" type="text" /></div></div>
@endforeach
</div>
@foreach (['WP_DEBUG' => false, 'WP_CACHE' => true, 'DISALLOW_FILE_EDIT' => true, 'FORCE_SSL_ADMIN' => true, 'AUTOMATIC_UPDATER_DISABLED' => false] as $k => $v)
<label class="clu-checkbox" style="justify-content:space-between;width:100%;"><span>{{ $k }}</span><input type="checkbox" @checked($v) /><span class="box"><svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3.5"><path d="M5 12l5 5L20 7"/></svg></span></label>
@endforeach
</div>
</div>
<div class="grid gap-[14px]" style="grid-template-columns:1fr 1fr;">
<div class="clu-card"><div class="clu-card-head"><h3>Datenbank</h3></div><div class="clu-card-body" style="display:flex;flex-direction:column;gap:10px;">
@foreach (['DB_NAME' => 'bergmann_prod', 'DB_USER' => 'bergmann_wp', 'DB_HOST' => '127.0.0.1:3306', 'DB_CHARSET' => 'utf8mb4'] as $k => $v)
<div class="flex flex-col gap-[6px]"><label class="text-[11.5px] text-(--color-muted) font-semibold">{{ $k }}</label><div class="clu-input"><input value="{{ $v }}" type="text" /></div></div>
@endforeach
</div></div>
<div class="clu-card"><div class="clu-card-head"><h3>Security Keys</h3></div><div class="clu-card-body" style="display:flex;flex-direction:column;gap:10px;">
<div class="flex justify-between text-[12.5px]"><span class="text-(--color-muted)">AUTH_KEY</span><span class="font-mono">env</span></div>
<div class="flex justify-between text-[12.5px]"><span class="text-(--color-muted)">SECURE_AUTH_KEY</span><span class="font-mono">env</span></div>
<div class="flex justify-between text-[12.5px]"><span class="text-(--color-muted)">Rotation</span><span class="font-mono">vor 11 Wochen</span></div>
<button class="clu-btn-primary" type="button" style="height:34px;padding:0 14px;align-self:flex-start;">Schlüssel rotieren</button>
</div></div>
</div>
</div>
@endif
@if ($tab === 'php')
<div class="clu-pane">
<div class="clu-card">
<div class="clu-card-head"><h3>php.ini · PHP {{ $site->php_version }}</h3></div>
<div class="clu-card-body" style="display:flex;flex-direction:column;gap:14px;">
<div class="clu-set-grid" style="row-gap:14px;">
@foreach (['memory_limit' => '256M', 'max_execution_time' => '60', 'max_input_time' => '60', 'upload_max_filesize' => '64M', 'post_max_size' => '64M', 'date.timezone' => 'Europe/Berlin', 'opcache.memory_consumption' => '256', 'opcache.max_accelerated_files' => '20000'] as $k => $v)
<div class="flex flex-col gap-[6px]"><label class="text-[11.5px] text-(--color-muted) font-semibold">{{ $k }}</label><div class="clu-input"><input value="{{ $v }}" type="text" /></div></div>
@endforeach
</div>
@foreach (['display_errors' => false, 'opcache.enable' => true, 'log_errors' => true, 'allow_url_fopen' => false] as $k => $v)
<label class="clu-checkbox" style="justify-content:space-between;width:100%;"><span>{{ $k }}</span><input type="checkbox" @checked($v) /><span class="box"><svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3.5"><path d="M5 12l5 5L20 7"/></svg></span></label>
@endforeach
</div>
</div>
<div class="grid gap-[14px]" style="grid-template-columns:1fr 1fr;">
<div class="clu-card"><div class="clu-card-head"><h3>OPcache</h3></div><div class="clu-card-body" style="display:flex;flex-direction:column;gap:9px;">
<div class="flex justify-between text-[12.5px]"><span class="text-(--color-muted)">Hit-Rate</span><span class="font-mono">99.2%</span></div>
<div class="flex justify-between text-[12.5px]"><span class="text-(--color-muted)">Scripts</span><span class="font-mono">4 218</span></div>
<div class="flex justify-between text-[12.5px]"><span class="text-(--color-muted)">Memory</span><span class="font-mono">187 / 256 MB</span></div>
<button class="clu-ghost-btn" type="button" style="align-self:flex-start;">Cache leeren</button>
</div></div>
<div class="clu-card"><div class="clu-card-head"><h3>Aktive Erweiterungen</h3></div><div class="clu-card-body"><div class="flex flex-wrap gap-[6px]">
@foreach (['curl', 'gd', 'imagick', 'mbstring', 'mysqli', 'opcache', 'redis', 'xml', 'zip', 'intl', 'json', 'soap', 'sodium', 'exif', 'fileinfo', 'iconv'] as $ext)<span class="clu-pill muted">{{ $ext }}</span>@endforeach
</div></div></div>
</div>
</div>
@endif
</div>