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

1546 lines
109 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

<?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';
/** Set of dir paths currently expanded in file tree. */
public array $openDirs = ['htdocs', 'htdocs/wp-content', 'htdocs/wp-content/themes', 'htdocs/wp-content/themes/storefront-child', 'logs'];
/** vhost / http / tls settings (in-memory for now). */
public string $httpVersion = 'http2'; // http1 | http2 | http3
public bool $hsts = true;
public int $hstsMaxAge = 31536000;
public bool $forceHttps = true;
public bool $brotli = true;
public string $tlsMin = '1.3';
public string $sslProvider = 'letsencrypt'; // letsencrypt | custom | cloudflare
/** Staging environment state. */
public bool $stagingActive = true;
public string $stagingSubdomain = 'staging';
public bool $stagingPasswordProtect = true;
public bool $stagingExcludeMedia = false;
public string $stagingLastSync = '2026-05-31 14:22';
/** UI-state for password reveal. */
public bool $showDbPassword = false;
/** Flash banner (notify). */
public ?string $flash = null;
public function notify(string $msg): void
{
$this->flash = $msg;
}
public function dismissFlash(): void { $this->flash = null; }
/** Redirect form state. */
public string $redirectStatus = '301'; // 301 | 302 | 307 | 410
public string $redirectMatch = 'Exact'; // Exact | Prefix | Regex | Wildcard
public function setRedirectStatus(string $s): void { $this->redirectStatus = $s; }
public function setRedirectMatch(string $m): void { $this->redirectMatch = $m; }
/** Generic settings bag for all toggle-rows across tabs. */
public array $settings = [
// vHost
'vhost.server_tokens' => true,
'vhost.http2_push' => false,
// WP-config toggles
'wp.debug' => false,
'wp.cache' => true,
'wp.disallow_file_edit' => true,
'wp.auto_updater_off' => false,
'wp.force_ssl_admin' => true,
// WP general
'wp.search_visible' => true,
'wp.registration' => false,
// PHP toggles
'php.display_errors' => false,
'php.opcache_enable' => true,
'php.log_errors' => true,
'php.allow_url_fopen' => true,
// SSL
'ssl.ocsp' => true,
'ssl.auto_renew' => true,
'ssl.caa_check' => true,
'ssl.ct_log' => true,
// SSH/FTP
'access.password_auth' => true, // password-auth DISABLED toggle
'access.no_root' => true,
'access.fail2ban' => true,
'access.ip_allowlist' => false,
'access.ssh_2fa' => false,
// Plugins
'wp.auto.security' => true,
'wp.auto.minor' => true,
'wp.auto.major' => false,
'wp.auto.snapshot' => true,
'wp.auto.staging_first' => false,
// Redirects new-rule form
'redirect.keep_qs' => true,
'redirect.case_insens' => false,
// Staging
'staging.robots_block' => true,
'staging.email_sandbox' => true,
'staging.payments_test' => true,
// Plugin auto-update flags (per slug)
'plugin.wordpress-seo.auto' => true,
'plugin.woocommerce.auto' => false,
'plugin.wp-rocket.auto' => true,
'plugin.wordfence.auto' => true,
'plugin.advanced-custom-fields.auto' => false,
'plugin.contact-form-7.auto' => true,
'plugin.akismet.auto' => true,
'plugin.updraftplus.auto' => false,
];
public function toggleSetting(string $key): void
{
$this->settings[$key] = ! ($this->settings[$key] ?? false);
}
public function isOn(string $key): bool
{
return (bool) ($this->settings[$key] ?? false);
}
/** Plugin / theme registry — mocked, would come from WP-API in prod. */
public function getPluginsProperty(): array
{
return [
['name' => 'Yoast SEO', 'slug' => 'wordpress-seo', 'version' => '22.5', 'latest' => '22.5', 'status' => 'active', 'auto' => true, 'updates' => false],
['name' => 'WooCommerce', 'slug' => 'woocommerce', 'version' => '8.9.2', 'latest' => '9.0.1', 'status' => 'active', 'auto' => false, 'updates' => true],
['name' => 'WP Rocket', 'slug' => 'wp-rocket', 'version' => '3.16.0', 'latest' => '3.16.0', 'status' => 'active', 'auto' => true, 'updates' => false],
['name' => 'Wordfence Security', 'slug' => 'wordfence', 'version' => '7.11.4', 'latest' => '7.11.6', 'status' => 'active', 'auto' => true, 'updates' => true],
['name' => 'Advanced Custom Fields', 'slug' => 'advanced-custom-fields', 'version' => '6.2.10', 'latest' => '6.3.1', 'status' => 'active', 'auto' => false, 'updates' => true],
['name' => 'Contact Form 7', 'slug' => 'contact-form-7', 'version' => '5.9.6', 'latest' => '5.9.6', 'status' => 'inactive', 'auto' => true, 'updates' => false],
['name' => 'Akismet Anti-Spam', 'slug' => 'akismet', 'version' => '5.3.2', 'latest' => '5.3.3', 'status' => 'active', 'auto' => true, 'updates' => true],
['name' => 'UpdraftPlus', 'slug' => 'updraftplus', 'version' => '1.24.0', 'latest' => '1.24.0', 'status' => 'inactive', 'auto' => false, 'updates' => false],
];
}
public function getThemesProperty(): array
{
return [
['name' => 'Storefront', 'version' => '4.6.0', 'latest' => '4.6.0', 'status' => 'parent'],
['name' => 'Storefront Child', 'version' => '1.2.1', 'latest' => '1.2.1', 'status' => 'active'],
['name' => 'Twenty Twenty-Four','version' => '1.2', 'latest' => '1.2', 'status' => 'inactive'],
];
}
public function getRedirectsProperty(): array
{
return [
['from' => '/alte-kategorie/', 'to' => '/produkte/kaffee/', 'code' => 301, 'hits' => 4218, 'last' => 'vor 12 min'],
['from' => '/2024-aktion', 'to' => '/aktuelle-angebote/', 'code' => 301, 'hits' => 2104, 'last' => 'vor 38 min'],
['from' => '/sale', 'to' => '/aktuelle-angebote/', 'code' => 302, 'hits' => 18642,'last' => 'vor 2 min'],
['from' => '/checkout-old', 'to' => '/warenkorb/', 'code' => 301, 'hits' => 12, 'last' => 'gestern'],
['from' => '/feed/atom', 'to' => '/feed/', 'code' => 301, 'hits' => 891, 'last' => 'vor 7 min'],
['from' => '/wp-content/uploads/2022/(.*)', 'to' => '/wp-content/uploads/legacy/$1', 'code' => 301, 'hits' => 56, 'last' => 'vor 4 std'],
];
}
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 toggleDir(string $path): void
{
if (in_array($path, $this->openDirs, true)) {
$this->openDirs = array_values(array_diff($this->openDirs, [$path]));
} else {
$this->openDirs[] = $path;
}
}
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 (folder toggle via Livewire). */
if (! function_exists('clu_render_tree')) {
function clu_render_tree(array $items, string $activeFile, array $openDirs, string $base = ''): string {
$out = '';
foreach ($items as $node) {
$isDir = ($node['type'] ?? '') === 'dir';
$myPath = ltrim($base . '/' . $node['name'], '/');
$isOpen = $isDir && in_array($myPath, $openDirs, true);
$isActive = ! $isDir && ($node['path'] ?? null) === $activeFile;
$rowClass = 'clu-tree-item' . ($isDir ? ' dir' : '') . ($isOpen ? ' open' : '') . ($isActive ? ' active' : '');
$click = $isDir
? 'wire:click="toggleDir(\''.addslashes($myPath).'\')"'
: 'wire:click="setActiveFile(\''.addslashes($node['path']).'\')"';
$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, $openDirs, $myPath).'</div>';
}
}
return $out;
}
}
@endphp
<div>
{{-- Flash banner --}}
@if ($flash)
<div class="clu-card mb-[14px]"
style="background:var(--color-success-soft);border-color:rgba(21,160,106,0.3);"
x-data x-init="setTimeout(() => $wire.dismissFlash(), 3500)">
<div class="clu-card-body" style="display:flex;align-items:center;gap:10px;padding:11px 14px;">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="color:var(--color-success);flex:none;"><circle cx="12" cy="12" r="9"/><path d="M9 12l2 2 4-4"/></svg>
<span class="flex-1 text-[13px]" style="color:var(--color-success);">{{ $flash }}</span>
<button type="button" wire:click="dismissFlash" style="color:var(--color-success);opacity:.7;">×</button>
</div>
</div>
@endif
{{-- ── 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" wire:click="notify('SSH-Benutzer angelegt')">
<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>
{{-- ── Grouped Sub-Sidebar + Content ── --}}
@php
$sections = [
'ÜBERBLICK' => [
'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"/>'],
],
'WORDPRESS' => [
'wp' => ['Einstellungen','<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"/>'],
'plugins' => ['Plugins', '<path d="M14 7V3H8v4M3 7h18v13H3zM12 11v6M9 14h6"/>'],
'cron' => ['Cronjobs', '<circle cx="12" cy="12" r="9"/><path d="M12 7v5l3 2"/>', $site->crons->count() ?: null],
'redirects' => ['Redirects', '<path d="M3 12h14l-4-4M17 12l-4 4"/>'],
],
'HOSTING' => [
'vhost' => ['vHost / nginx','<path d="M2 12h20M2 6h20M2 18h20"/>'],
'php' => ['PHP / ini', '<path d="M4 7h16M4 12h16M4 17h10"/>'],
'ssl' => ['SSL / TLS', '<rect x="5" y="11" width="14" height="9" rx="2"/><path d="M8 11V7a4 4 0 0 1 8 0v4"/>'],
'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"/>'],
'staging' => ['Staging', '<path d="M3 12a9 9 0 1 0 9-9M3 4v5h5"/>'],
],
'ZUGANG' => [
'access' => ['SSH / FTP', '<path d="M4 17l5-5-5-5M13 19h7"/>'],
],
'BEOBACHTEN' => [
'logs' => ['Logs', '<rect x="3" y="3" width="18" height="18" rx="2"/><path d="M7 8h10M7 12h10M7 16h6"/>'],
],
];
@endphp
<div class="clu-detail-layout">
<nav class="clu-detail-nav">
@foreach ($sections as $sectionTitle => $items)
<div class="clu-detail-section-title">{{ $sectionTitle }}</div>
@foreach ($items as $key => $meta)
@php [$label, $svg, $count] = array_pad((array) $meta, 3, null); @endphp
<a href="?tab={{ $key }}" wire:click.prevent="setTab('{{ $key }}')"
class="clu-detail-item @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 ($count)
<span class="count">{{ $count }}</span>
@endif
</a>
@endforeach
@endforeach
</nav>
<div class="clu-detail-content">
{{-- ────── 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">
<div class="clu-toggle-row" wire:click="toggleMaintenance" style="cursor:pointer;">
<div class="clu-tr-body"><div class="clu-tr-title">Wartungsmodus</div><div class="clu-tr-desc">site für besucher sperren</div></div>
<span class="clu-toggle @if($site->maintenance_mode) on @endif"></span>
</div>
<div class="clu-toggle-row" wire:click="toggleCache" style="cursor:pointer;">
<div class="clu-tr-body"><div class="clu-tr-title">Objekt-Cache (Redis)</div><div class="clu-tr-desc">datenbankabfragen zwischenspeichern</div></div>
<span class="clu-toggle @if($site->cache_enabled) on @endif"></span>
</div>
<div class="clu-toggle-row" wire:click="toggleCdn" style="cursor:pointer;">
<div class="clu-tr-body"><div class="clu-tr-title">Edge-CDN</div><div class="clu-tr-desc">statische inhalte global cachen</div></div>
<span class="clu-toggle @if($site->cdn_enabled) on @endif"></span>
</div>
<div class="clu-toggle-row" style="cursor:pointer;">
<div class="clu-tr-body"><div class="clu-tr-title">HTTPS erzwingen</div><div class="clu-tr-desc">alle anfragen auf ssl umleiten</div></div>
<span class="clu-toggle @if($site->https_enabled) on @endif"></span>
</div>
</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" wire:click="notify('SFTP-Benutzer angelegt')"><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" wire:click="notify('Plugin-Browser geöffnet')"><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" wire:click="notify('Clone Production → Staging gestartet')"><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, $openDirs) !!}
</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" wire:click="notify('Änderungen verworfen')">Verwerfen</button>
<button class="clu-btn-primary" type="button" style="height:36px;padding:0 16px;" wire:click="notify('gespeichert')">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;" wire:click="notify('Push Staging → Production gestartet (mit Snapshot)')">
<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
{{-- ────── PLUGINS (Plugin- + Theme-Manager) ────── --}}
@if ($tab === 'plugins')
<div class="clu-pane">
<div class="grid gap-[14px]" style="grid-template-columns:repeat(auto-fit,minmax(220px,1fr));">
@php
$totalPlugins = count($this->plugins);
$activePlugins = count(array_filter($this->plugins, fn($p) => $p['status'] === 'active'));
$updatesPending = count(array_filter($this->plugins, fn($p) => $p['updates']));
$autoUpdate = count(array_filter($this->plugins, fn($p) => $p['auto']));
@endphp
<div class="clu-metric"><div class="clu-metric-row"><span class="clu-metric-label">Plugins gesamt</span></div><div class="clu-metric-value">{{ $totalPlugins }}</div><div class="clu-metric-delta">{{ $activePlugins }} aktiv</div></div>
<div class="clu-metric @if($updatesPending > 0) warning @endif"><div class="clu-metric-row"><span class="clu-metric-label">Updates verfügbar</span></div><div class="clu-metric-value">{{ $updatesPending }}</div><div class="clu-metric-delta @if($updatesPending > 0) warn @endif">{{ $updatesPending > 0 ? 'jetzt aktualisieren' : 'alles aktuell' }}</div></div>
<div class="clu-metric"><div class="clu-metric-row"><span class="clu-metric-label">Auto-Update aktiv</span></div><div class="clu-metric-value">{{ $autoUpdate }}<span class="unit">/{{ $totalPlugins }}</span></div><div class="clu-metric-delta">automatisch gepflegt</div></div>
<div class="clu-metric success"><div class="clu-metric-row"><span class="clu-metric-label">Themes</span></div><div class="clu-metric-value">{{ count($this->themes) }}</div><div class="clu-metric-delta">1 aktiv (child)</div></div>
</div>
<div class="clu-card">
<div class="clu-card-head">
<h3 class="flex items-center gap-[8px]">
<svg class="clu-hico" width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14 7V3H8v4M3 7h18v13H3zM12 11v6M9 14h6"/></svg>
Plugins
</h3>
<div class="flex gap-[8px]">
<button class="clu-ghost-btn" type="button" wire:click="notify('Plugin-Upload gestartet')">Plugin hochladen</button>
<button class="clu-btn-primary" type="button" style="height:32px;padding:0 12px;font-size:12px;">
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4"><path d="M12 5v14M5 12h14"/></svg>
Aus Repo
</button>
</div>
</div>
<div class="clu-card-body tight" style="overflow-x:auto;">
<table class="clu-table">
<thead><tr><th>Plugin</th><th>Version</th><th>Status</th><th>Auto-Update</th><th></th></tr></thead>
<tbody>
@foreach ($this->plugins as $p)
<tr>
<td>
<div class="clu-site-cell">
<div class="clu-favicon" style="background:linear-gradient(135deg,#5a8bff,#8a63ff);">{{ mb_strtoupper(mb_substr($p['name'], 0, 1)) }}</div>
<div class="clu-site-meta">
<span class="clu-site-domain">{{ $p['name'] }}</span>
<span class="clu-site-server">{{ $p['slug'] }}</span>
</div>
</div>
</td>
<td>
@if ($p['updates'])
<span class="clu-pill warning">{{ $p['version'] }} → {{ $p['latest'] }}</span>
@else
<span class="clu-pill success">{{ $p['version'] }}</span>
@endif
</td>
<td>
@if ($p['status'] === 'active')<span class="clu-health">aktiv</span>
@else <span class="clu-health warn">inaktiv</span>@endif
</td>
<td>
<span class="clu-toggle @if($this->isOn('plugin.'.$p['slug'].'.auto')) on @endif"
wire:click="toggleSetting('plugin.{{ $p['slug'] }}.auto')"
style="cursor:pointer;"></span>
</td>
<td>
<div class="clu-row-actions">
@if ($p['updates'])
<button type="button" title="Update auf {{ $p['latest'] }}"><svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 12a9 9 0 1 1-3-6.7M21 4v5h-5"/></svg></button>
@endif
<button type="button" title="{{ $p['status'] === 'active' ? 'Deaktivieren' : 'Aktivieren' }}"><svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">@if($p['status']==='active')<rect x="6" y="6" width="12" height="12" rx="1"/>@else<polygon points="6 4 20 12 6 20 6 4"/>@endif</svg></button>
<button type="button" title="Löschen"><svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 6h18M8 6V4h8v2M19 6l-1 14H6L5 6"/></svg></button>
</div>
</td>
</tr>
@endforeach
</tbody>
</table>
</div>
@if ($updatesPending > 0)
<div class="clu-form-actions" style="background:var(--color-warning-soft);">
<span class="text-[12.5px]"><b>{{ $updatesPending }}</b> Updates verfügbar. Bulk-Update führt vorher automatisch einen Snapshot durch.</span>
<button class="clu-btn-primary" type="button" style="height:32px;padding:0 12px;font-size:12px;" wire:click="notify('Bulk-Update gestartet (Snapshot zuerst)')">Alle aktualisieren ({{ $updatesPending }})</button>
</div>
@endif
</div>
<div class="clu-card">
<div class="clu-card-head">
<h3>Themes</h3>
<button class="clu-ghost-btn" type="button" wire:click="notify('Theme-Upload gestartet')">Theme hochladen</button>
</div>
<div class="clu-card-body tight" style="overflow-x:auto;">
<table class="clu-table">
<thead><tr><th>Theme</th><th>Version</th><th>Status</th><th></th></tr></thead>
<tbody>
@foreach ($this->themes as $t)
<tr>
<td class="font-semibold">{{ $t['name'] }}</td>
<td><span class="clu-pill success">{{ $t['version'] }}</span></td>
<td>
@if ($t['status'] === 'active')<span class="clu-health">aktiv</span>
@elseif ($t['status'] === 'parent')<span class="clu-pill muted">parent</span>
@else <span class="clu-health warn">inaktiv</span>@endif
</td>
<td>
<div class="clu-row-actions">
<button type="button" title="Aktivieren"><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>
@endforeach
</tbody>
</table>
</div>
</div>
<div class="clu-card">
<div class="clu-card-head"><h3>Update-Policy</h3></div>
<div class="clu-card-body">
<div class="clu-toggle-row" wire:click="toggleSetting('wp.auto.security')" style="cursor:pointer;">
<div class="clu-tr-body"><div class="clu-tr-title">Auto-Update: Security-Patches</div><div class="clu-tr-desc">kritische sicherheits-updates automatisch einspielen</div></div>
<span class="clu-toggle @if($this->isOn('wp.auto.security')) on @endif"></span>
</div>
<div class="clu-toggle-row" wire:click="toggleSetting('wp.auto.minor')" style="cursor:pointer;">
<div class="clu-tr-body"><div class="clu-tr-title">Auto-Update: Minor-Versionen</div><div class="clu-tr-desc">8.9.2 → 8.9.4 automatisch (gleiche major)</div></div>
<span class="clu-toggle @if($this->isOn('wp.auto.minor')) on @endif"></span>
</div>
<div class="clu-toggle-row" wire:click="toggleSetting('wp.auto.major')" style="cursor:pointer;">
<div class="clu-tr-body"><div class="clu-tr-title">Auto-Update: Major-Versionen</div><div class="clu-tr-desc">8.x → 9.x — manuell empfohlen, breaking changes möglich</div></div>
<span class="clu-toggle @if($this->isOn('wp.auto.major')) on @endif"></span>
</div>
<div class="clu-toggle-row" wire:click="toggleSetting('wp.auto.snapshot')" style="cursor:pointer;">
<div class="clu-tr-body"><div class="clu-tr-title">Snapshot vor jedem Update</div><div class="clu-tr-desc">automatisches backup vor plugin-update, rollback bei fehler</div></div>
<span class="clu-toggle @if($this->isOn('wp.auto.snapshot')) on @endif"></span>
</div>
<div class="clu-toggle-row" wire:click="toggleSetting('wp.auto.staging_first')" style="cursor:pointer;">
<div class="clu-tr-body"><div class="clu-tr-title">Staging-Test vor Production</div><div class="clu-tr-desc">update zuerst auf staging, smoke-test, dann auf prod</div></div>
<span class="clu-toggle @if($this->isOn('wp.auto.staging_first')) on @endif"></span>
</div>
</div>
</div>
</div>
@endif
{{-- ────── REDIRECTS ────── --}}
@if ($tab === 'redirects')
<div class="clu-pane">
<div class="grid gap-[14px]" style="grid-template-columns:repeat(auto-fit,minmax(220px,1fr));">
@php
$totalRedirects = count($this->redirects);
$totalHits = array_sum(array_column($this->redirects, 'hits'));
@endphp
<div class="clu-metric"><div class="clu-metric-row"><span class="clu-metric-label">Aktive Regeln</span></div><div class="clu-metric-value">{{ $totalRedirects }}</div><div class="clu-metric-delta">301 + 302</div></div>
<div class="clu-metric success"><div class="clu-metric-row"><span class="clu-metric-label">Treffer / 24h</span></div><div class="clu-metric-value">{{ number_format($totalHits, 0, ',', '.') }}</div><div class="clu-metric-delta">erfolgreich weitergeleitet</div></div>
<div class="clu-metric warning"><div class="clu-metric-row"><span class="clu-metric-label">404-Quellen</span></div><div class="clu-metric-value">7</div><div class="clu-metric-delta warn">potenzielle redirect-targets</div></div>
<div class="clu-metric"><div class="clu-metric-row"><span class="clu-metric-label">Loop-Detection</span></div><div class="clu-metric-value">0</div><div class="clu-metric-delta">keine zirkelschlüsse</div></div>
</div>
<div class="clu-card">
<div class="clu-card-head">
<h3 class="flex items-center gap-[8px]">
<svg class="clu-hico" width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 12h14l-4-4M17 12l-4 4"/></svg>
Neue Redirect-Regel
</h3>
</div>
<div class="clu-card-body">
<div class="clu-set-grid">
<div class="clu-field">
<label>Von (Source)</label>
<div class="clu-input"><input placeholder="/alte-url oder /pattern/(.*)" style="font-family:var(--font-mono);" type="text" /></div>
<span class="clu-tr-desc">Regex erlaubt mit Capture-Groups ($1, $2).</span>
</div>
<div class="clu-field">
<label>Nach (Target)</label>
<div class="clu-input"><input placeholder="/neue-url" style="font-family:var(--font-mono);" type="text" /></div>
<span class="clu-tr-desc">Externe URLs (https://...) erlaubt.</span>
</div>
<div class="clu-field">
<label>Status-Code</label>
<div class="flex gap-[8px] flex-wrap">
@foreach (['301' => '301 (Permanent)', '302' => '302 (Temporär)', '307' => '307 (Strict)', '410' => '410 (Gone)'] as $code => $lbl)
<button type="button" wire:click="setRedirectStatus('{{ $code }}')"
class="clu-ghost-btn"
style="@if((string)$code === $redirectStatus) background:var(--color-accent-soft);border-color:var(--color-accent);color:var(--color-accent); @endif">
{{ $lbl }}
</button>
@endforeach
</div>
</div>
<div class="clu-field">
<label>Match-Typ</label>
<select wire:model.live="redirectMatch" class="clu-input" style="appearance:auto;font-family:var(--font-mono);">
<option value="Exact">Exact</option>
<option value="Prefix">Prefix</option>
<option value="Regex">Regex</option>
<option value="Wildcard">Wildcard</option>
</select>
</div>
</div>
<div style="margin-top:8px;">
<div class="clu-toggle-row" wire:click="toggleSetting('redirect.keep_qs')" style="cursor:pointer;">
<div class="clu-tr-body"><div class="clu-tr-title">Query-String beibehalten</div><div class="clu-tr-desc">?utm_source=… überträgt sich auf target</div></div>
<span class="clu-toggle @if($this->isOn('redirect.keep_qs')) on @endif"></span>
</div>
<div class="clu-toggle-row" wire:click="toggleSetting('redirect.case_insens')" style="cursor:pointer;">
<div class="clu-tr-body"><div class="clu-tr-title">Case-insensitive Match</div><div class="clu-tr-desc">/PRODUKT und /produkt matchen gleich</div></div>
<span class="clu-toggle @if($this->isOn('redirect.case_insens')) on @endif"></span>
</div>
</div>
</div>
<div class="clu-form-actions">
<button class="clu-ghost-btn" type="button" wire:click="notify('CSV-Import gestartet')">Aus CSV importieren</button>
<button class="clu-btn-primary" type="button" style="height:36px;padding:0 14px;" wire:click="notify('Redirect-Regel hinzugefügt')">Regel hinzufügen</button>
</div>
</div>
<div class="clu-card">
<div class="clu-card-head">
<h3>Aktive Regeln <span class="clu-pill muted">{{ $totalRedirects }}</span></h3>
<span class="meta">nach hits sortiert</span>
</div>
<div class="clu-card-body tight" style="overflow-x:auto;">
<table class="clu-table">
<thead><tr><th>Von</th><th>→</th><th>Nach</th><th>Code</th><th>Hits 24h</th><th>Letzter Treffer</th><th></th></tr></thead>
<tbody>
@foreach ($this->redirects as $r)
<tr>
<td class="font-mono text-[12px]" style="max-width:300px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">{{ $r['from'] }}</td>
<td style="color:var(--color-muted-2);"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M5 12h14M13 5l7 7-7 7"/></svg></td>
<td class="font-mono text-[12px]" style="max-width:300px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">{{ $r['to'] }}</td>
<td><span class="clu-pill @if($r['code'] === 301) success @else accent @endif">{{ $r['code'] }}</span></td>
<td class="font-mono">{{ number_format($r['hits'], 0, ',', '.') }}</td>
<td class="font-mono text-[11.5px] text-(--color-muted)">{{ $r['last'] }}</td>
<td>
<div class="clu-row-actions">
<button type="button" title="Bearbeiten"><svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M11 4H4v16h16v-7M18 2l4 4-11 11H7v-4z"/></svg></button>
<button type="button" title="Test"><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="Löschen"><svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 6h18M8 6V4h8v2M19 6l-1 14H6L5 6"/></svg></button>
</div>
</td>
</tr>
@endforeach
</tbody>
</table>
</div>
</div>
</div>
@endif
{{-- ────── STAGING ────── --}}
@if ($tab === 'staging')
<div class="clu-pane">
<div class="clu-card">
<div class="clu-card-head">
<h3 class="flex items-center gap-[8px]">
<svg class="clu-hico" width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 12a9 9 0 1 0 9-9M3 4v5h5M21 12a9 9 0 1 0-9 9M21 20v-5h-5"/></svg>
Staging-Environment
</h3>
@if ($stagingActive)
<span class="clu-pill success">aktiv</span>
@else
<span class="clu-pill muted">nicht angelegt</span>
@endif
</div>
<div class="clu-card-body">
<div class="clu-set-grid">
<div class="clu-field">
<label>Staging-Subdomain</label>
<div class="clu-input-group">
<input wire:model.live="stagingSubdomain" type="text" style="font-family:var(--font-mono);" />
<span class="suffix">.{{ $site->domain }}</span>
</div>
<span class="clu-tr-desc">URL: <b>{{ $stagingSubdomain }}.{{ $site->domain }}</b></span>
</div>
<div class="clu-field">
<label>Server</label>
<select class="clu-input" style="appearance:auto;">
<option value="{{ $site->server->name }}">{{ $site->server->name }} (gleicher Server)</option>
<option>staging-pool (dedicated)</option>
</select>
<span class="clu-tr-desc">Staging auf gleichem Server spart Ressourcen — Lasttests ggf. eigenen Server.</span>
</div>
<div class="clu-field">
<label>Letzter Sync</label>
<div class="clu-input"><input value="{{ $stagingLastSync }}" disabled style="font-family:var(--font-mono);" type="text" /></div>
</div>
<div class="clu-field">
<label>Staging-Größe</label>
<div class="clu-input"><input value="2.8 GB · 142 Tabellen · 8.4k Dateien" disabled type="text" /></div>
</div>
</div>
<div style="margin-top:8px;">
<div class="clu-toggle-row" wire:click="$toggle('stagingPasswordProtect')" style="cursor:pointer;">
<div class="clu-tr-body"><div class="clu-tr-title">HTTP-Basic-Auth</div><div class="clu-tr-desc">staging-zugriff per username + passwort schützen (verhindert google-indexierung)</div></div>
<span class="clu-toggle @if($stagingPasswordProtect) on @endif"></span>
</div>
<div class="clu-toggle-row" wire:click="toggleSetting('staging.robots_block')" style="cursor:pointer;">
<div class="clu-tr-body"><div class="clu-tr-title">robots.txt blockt alle Crawler</div><div class="clu-tr-desc">User-agent: * / Disallow: /</div></div>
<span class="clu-toggle @if($this->isOn('staging.robots_block')) on @endif"></span>
</div>
<div class="clu-toggle-row" wire:click="$toggle('stagingExcludeMedia')" style="cursor:pointer;">
<div class="clu-tr-body"><div class="clu-tr-title">Media-Dateien beim Sync ausschließen</div><div class="clu-tr-desc">spart bandwidth, lädt /uploads lazy on-demand</div></div>
<span class="clu-toggle @if($stagingExcludeMedia) on @endif"></span>
</div>
<div class="clu-toggle-row" wire:click="toggleSetting('staging.email_sandbox')" style="cursor:pointer;">
<div class="clu-tr-body"><div class="clu-tr-title">Email-Sandbox (DISALLOW_OUTGOING_MAIL)</div><div class="clu-tr-desc">keine kunden-mails von staging versenden</div></div>
<span class="clu-toggle @if($this->isOn('staging.email_sandbox')) on @endif"></span>
</div>
<div class="clu-toggle-row" wire:click="toggleSetting('staging.payments_test')" style="cursor:pointer;">
<div class="clu-tr-body"><div class="clu-tr-title">Payment-Gateways auf Test-Mode</div><div class="clu-tr-desc">stripe + paypal automatisch im sandbox-mode</div></div>
<span class="clu-toggle @if($this->isOn('staging.payments_test')) on @endif"></span>
</div>
</div>
</div>
<div class="clu-form-actions">
@if ($stagingActive)
<button class="clu-ghost-btn" type="button" style="color:var(--color-danger);border-color:rgba(219,59,59,0.3);" wire:click="notify('Staging-Environment gelöscht')">Staging löschen</button>
@endif
<button class="clu-ghost-btn" type="button" wire:click="notify('Staging-Settings gespeichert')">Settings speichern</button>
</div>
</div>
{{-- Sync actions --}}
<div class="grid gap-[14px]" style="grid-template-columns:1fr 1fr;">
<div class="clu-card">
<div class="clu-card-head"><h3>Production → Staging</h3><span class="clu-pill accent">CLONE</span></div>
<div class="clu-card-body" style="display:flex;flex-direction:column;gap:12px;">
<p class="clu-tr-desc">Vollständiger Clone von Production: DB + Files + Uploads. URLs werden automatisch auf staging.{{ $site->domain }} umgeschrieben.</p>
<div class="clu-kv"><span class="k">Schätzung</span><span class="v">~4 min · 2.8 GB</span></div>
<div class="clu-kv" style="border-bottom:0;"><span class="k">Letzter Lauf</span><span class="v">{{ $stagingLastSync }}</span></div>
<button class="clu-btn-primary" type="button" style="height:38px;padding:0 16px;align-self:flex-start;">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="M3 12a9 9 0 1 0 9-9M3 4v5h5"/></svg>
Jetzt klonen
</button>
</div>
</div>
<div class="clu-card">
<div class="clu-card-head"><h3>Staging → Production</h3><span class="clu-pill warning">PUSH</span></div>
<div class="clu-card-body" style="display:flex;flex-direction:column;gap:12px;">
<p class="clu-tr-desc">Selektive Übernahme: Code, DB-Tabellen oder beides. <b>Auto-Snapshot von Production</b> vor dem Push.</p>
<div class="flex flex-wrap gap-[6px]">
<span class="clu-pill muted">✓ Plugins</span>
<span class="clu-pill muted">✓ Themes</span>
<span class="clu-pill muted">✓ wp_options</span>
<span class="clu-pill muted">✗ wp_posts</span>
<span class="clu-pill muted">✗ wp_users</span>
</div>
<button class="clu-btn-primary clu-btn-danger" type="button" style="height:38px;padding:0 16px;align-self:flex-start;">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="M5 12h14M13 5l7 7-7 7"/></svg>
Push to Production
</button>
</div>
</div>
</div>
{{-- Recent activity --}}
<div class="clu-card">
<div class="clu-card-head"><h3>Letzte Sync-Operationen</h3></div>
<div class="clu-card-body tight">
<div class="clu-feed">
<div class="clu-feed-row"><div class="clu-feed-ico success"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="9"/><path d="M9 12l2 2 4-4"/></svg></div><div class="clu-feed-body"><div class="clu-feed-msg">Push: Plugins + Themes nach Production · 142 MB übertragen</div><div class="clu-feed-meta">heute, 14:22 · Marie Weber · 3m 18s</div></div></div>
<div class="clu-feed-row"><div class="clu-feed-ico"><svg width="14" height="14" 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></div><div class="clu-feed-body"><div class="clu-feed-msg">Clone: Production → Staging · 2.8 GB · automatisch</div><div class="clu-feed-meta">heute, 06:00 · Scheduler · 4m 02s</div></div></div>
<div class="clu-feed-row"><div class="clu-feed-ico warning"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M10.3 3.86l-8.55 14.83A2 2 0 0 0 3.46 22h17.08a2 2 0 0 0 1.71-3.31L13.7 3.86a2 2 0 0 0-3.4 0z"/><path d="M12 9v4M12 17h.01"/></svg></div><div class="clu-feed-body"><div class="clu-feed-msg">Push abgebrochen: WP-Version-Mismatch (6.6 ↔ 6.4)</div><div class="clu-feed-meta">gestern, 18:14 · Marie Weber</div></div></div>
<div class="clu-feed-row"><div class="clu-feed-ico success"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="9"/><path d="M9 12l2 2 4-4"/></svg></div><div class="clu-feed-body"><div class="clu-feed-msg">Clone: Production → Staging · WooCommerce-Update testen</div><div class="clu-feed-meta">gestern, 11:42 · Marie Weber · 3m 56s</div></div></div>
</div>
</div>
</div>
</div>
@endif
{{-- ────── VHOST ────── --}}
@if ($tab === 'vhost')
<div class="clu-pane">
<div class="clu-card">
<div class="clu-card-head">
<h3 class="flex items-center gap-[8px]">
<svg class="clu-hico" width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M2 12h20M2 6h20M2 18h20"/></svg>
Virtual Host
</h3>
<span class="meta">{{ $site->domain }} · nginx</span>
</div>
<div class="clu-card-body">
{{-- HTTP version --}}
<div class="clu-field">
<label>HTTP-Version</label>
<div class="flex gap-[8px]">
@foreach (['http1' => 'HTTP/1.1', 'http2' => 'HTTP/2', 'http3' => 'HTTP/3 (QUIC)'] as $v => $lbl)
<button type="button" wire:click="$set('httpVersion', '{{ $v }}')"
class="clu-ghost-btn"
style="@if($httpVersion === $v) background:var(--color-accent-soft);border-color:var(--color-accent);color:var(--color-accent); @endif">
{{ $lbl }}
</button>
@endforeach
</div>
<span class="clu-tr-desc">HTTP/3 nutzt QUIC über UDP — schneller bei verlustbehafteten Verbindungen.</span>
</div>
<div class="clu-set-grid">
<div class="clu-field">
<label>Server-Name</label>
<div class="clu-input"><input value="{{ $site->domain }}" style="font-family:var(--font-mono);" type="text" /></div>
</div>
<div class="clu-field">
<label>Document Root</label>
<div class="clu-input"><input value="/var/www/bergmann/htdocs" style="font-family:var(--font-mono);" type="text" /></div>
</div>
<div class="clu-field">
<label>client_max_body_size</label>
<div class="clu-input-group"><input value="128" type="text" /><span class="suffix">M</span></div>
</div>
<div class="clu-field">
<label>keepalive_timeout</label>
<div class="clu-input-group"><input value="65" type="text" /><span class="suffix">sek</span></div>
</div>
<div class="clu-field">
<label>worker_connections</label>
<div class="clu-input"><input value="2048" style="font-family:var(--font-mono);" type="text" /></div>
</div>
<div class="clu-field">
<label>access_log</label>
<div class="clu-input"><input value="/var/log/nginx/bergmann.access.log" style="font-family:var(--font-mono);" type="text" /></div>
</div>
</div>
<div style="margin-top:8px;">
<div class="clu-toggle-row" wire:click="$toggle('forceHttps')" style="cursor:pointer;">
<div class="clu-tr-body"><div class="clu-tr-title">HTTP → HTTPS Redirect</div><div class="clu-tr-desc">alle :80 anfragen mit 301 auf https umleiten</div></div>
<span class="clu-toggle @if($forceHttps) on @endif"></span>
</div>
<div class="clu-toggle-row" wire:click="$toggle('hsts')" style="cursor:pointer;">
<div class="clu-tr-body"><div class="clu-tr-title">HSTS (Strict-Transport-Security)</div><div class="clu-tr-desc">browser merkt sich https-only für {{ number_format($hstsMaxAge / 86400, 0, ',', '.') }} tage</div></div>
<span class="clu-toggle @if($hsts) on @endif"></span>
</div>
<div class="clu-toggle-row" wire:click="$toggle('brotli')" style="cursor:pointer;">
<div class="clu-tr-body"><div class="clu-tr-title">Brotli + gzip Kompression</div><div class="clu-tr-desc">text-assets vor auslieferung komprimieren</div></div>
<span class="clu-toggle @if($brotli) on @endif"></span>
</div>
<div class="clu-toggle-row" wire:click="toggleSetting('vhost.server_tokens')" style="cursor:pointer;">
<div class="clu-tr-body"><div class="clu-tr-title">server_tokens off</div><div class="clu-tr-desc">nginx-version aus header entfernen</div></div>
<span class="clu-toggle @if($this->isOn('vhost.server_tokens')) on @endif"></span>
</div>
<div class="clu-toggle-row" wire:click="toggleSetting('vhost.http2_push')" style="cursor:pointer;">
<div class="clu-tr-body"><div class="clu-tr-title">HTTP/2 Server Push</div><div class="clu-tr-desc">kritische assets im voraus an client streamen</div></div>
<span class="clu-toggle @if($this->isOn('vhost.http2_push')) on @endif"></span>
</div>
</div>
</div>
<div class="clu-form-actions">
<button class="clu-ghost-btn" type="button" wire:click="notify('nginx -t: configuration test successful')">nginx -t (Syntax-Check)</button>
<button class="clu-btn-primary" type="button" style="height:36px;padding:0 14px;" wire:click="notify('vHost gespeichert + nginx reload')">vHost speichern + reload</button>
</div>
</div>
<div class="grid gap-[14px]" style="grid-template-columns:1fr 1fr;">
<div class="clu-card">
<div class="clu-card-head"><h3>Custom Headers</h3></div>
<div class="clu-card-body" style="display:flex;flex-direction:column;gap:9px;">
<div class="clu-kv"><span class="k">X-Frame-Options</span><span class="v">SAMEORIGIN</span></div>
<div class="clu-kv"><span class="k">X-Content-Type-Options</span><span class="v">nosniff</span></div>
<div class="clu-kv"><span class="k">Referrer-Policy</span><span class="v">strict-origin</span></div>
<div class="clu-kv" style="border-bottom:0;"><span class="k">Permissions-Policy</span><span class="v">camera=(), mic=()</span></div>
</div>
</div>
<div class="clu-card">
<div class="clu-card-head"><h3>Rate Limit</h3><span class="clu-pill success">aktiv</span></div>
<div class="clu-card-body" style="display:flex;flex-direction:column;gap:9px;">
<div class="clu-kv"><span class="k">Zone-Größe</span><span class="v">10 MB</span></div>
<div class="clu-kv"><span class="k">/wp-login.php</span><span class="v">5 req / min</span></div>
<div class="clu-kv"><span class="k">/xmlrpc.php</span><span class="v">geblockt</span></div>
<div class="clu-kv" style="border-bottom:0;"><span class="k">Burst</span><span class="v">20</span></div>
</div>
</div>
</div>
</div>
@endif
{{-- ────── SSL / TLS ────── --}}
@if ($tab === 'ssl')
<div class="clu-pane">
<div class="clu-card">
<div class="clu-card-head">
<h3 class="flex items-center gap-[8px]">
<svg class="clu-hico" width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="5" y="11" width="14" height="9" rx="2"/><path d="M8 11V7a4 4 0 0 1 8 0v4"/></svg>
SSL / TLS Zertifikat
</h3>
<span class="clu-pill success">gültig · 54 tage</span>
</div>
<div class="clu-card-body">
<div class="clu-field">
<label>Cert-Provider</label>
<div class="flex gap-[8px] flex-wrap">
@foreach (['letsencrypt' => "Let's Encrypt", 'cloudflare' => 'Cloudflare Origin', 'custom' => 'Custom Upload'] as $v => $lbl)
<button type="button" wire:click="$set('sslProvider', '{{ $v }}')"
class="clu-ghost-btn"
style="@if($sslProvider === $v) background:var(--color-accent-soft);border-color:var(--color-accent);color:var(--color-accent); @endif">
{{ $lbl }}
</button>
@endforeach
</div>
</div>
<div class="clu-set-grid">
<div class="clu-field">
<label>Common Name (CN)</label>
<div class="clu-input"><input value="{{ $site->domain }}" style="font-family:var(--font-mono);" type="text" /></div>
</div>
<div class="clu-field">
<label>Subject Alternative Names (SAN)</label>
<div class="clu-input"><input value="www.{{ $site->domain }}, *.{{ $site->domain }}" style="font-family:var(--font-mono);" type="text" /></div>
</div>
<div class="clu-field">
<label>TLS-Version (min)</label>
<div class="flex gap-[8px]">
@foreach (['1.2' => 'TLS 1.2', '1.3' => 'TLS 1.3'] as $v => $lbl)
<button type="button" wire:click="$set('tlsMin', '{{ $v }}')"
class="clu-ghost-btn"
style="@if($tlsMin === $v) background:var(--color-accent-soft);border-color:var(--color-accent);color:var(--color-accent); @endif">
{{ $lbl }}
</button>
@endforeach
</div>
</div>
<div class="clu-field">
<label>Cipher Suite Profile</label>
<select class="clu-input" style="appearance:auto;font-family:var(--font-mono);">
<option>Mozilla Modern</option>
<option selected>Mozilla Intermediate</option>
<option>Mozilla Old (PCI)</option>
<option>Custom</option>
</select>
</div>
</div>
<div style="margin-top:8px;">
<div class="clu-toggle-row" wire:click="toggleSetting('ssl.ocsp')" style="cursor:pointer;">
<div class="clu-tr-body"><div class="clu-tr-title">OCSP Stapling</div><div class="clu-tr-desc">cert-revocation-status mit response cachen</div></div>
<span class="clu-toggle @if($this->isOn('ssl.ocsp')) on @endif"></span>
</div>
<div class="clu-toggle-row" wire:click="toggleSetting('ssl.auto_renew')" style="cursor:pointer;">
<div class="clu-tr-body"><div class="clu-tr-title">Auto-Renewal</div><div class="clu-tr-desc">erneuerung 30 tage vor ablauf</div></div>
<span class="clu-toggle @if($this->isOn('ssl.auto_renew')) on @endif"></span>
</div>
<div class="clu-toggle-row" wire:click="toggleSetting('ssl.caa_check')" style="cursor:pointer;">
<div class="clu-tr-body"><div class="clu-tr-title">CAA-Record-Check</div><div class="clu-tr-desc">vor neuausstellung dns-caa prüfen</div></div>
<span class="clu-toggle @if($this->isOn('ssl.caa_check')) on @endif"></span>
</div>
<div class="clu-toggle-row" wire:click="toggleSetting('ssl.ct_log')" style="cursor:pointer;">
<div class="clu-tr-body"><div class="clu-tr-title">Certificate Transparency Log</div><div class="clu-tr-desc">cert in öffentliches ct-log eintragen</div></div>
<span class="clu-toggle @if($this->isOn('ssl.ct_log')) on @endif"></span>
</div>
</div>
</div>
<div class="clu-form-actions">
<button class="clu-ghost-btn" type="button" wire:click="notify('Zertifikat heruntergeladen (.pem)')">Cert herunterladen</button>
<button class="clu-ghost-btn" type="button" wire:click="notify('Zertifikat-Erneuerung läuft (ACME)')">Jetzt erneuern</button>
<button class="clu-btn-primary" type="button" style="height:36px;padding:0 14px;" wire:click="notify('TLS-Settings gespeichert + nginx reload')">Speichern + reload</button>
</div>
</div>
<div class="grid gap-[14px]" style="grid-template-columns:1fr 1fr;">
<div class="clu-card">
<div class="clu-card-head"><h3>Cert Details</h3></div>
<div class="clu-card-body" style="display:flex;flex-direction:column;gap:9px;">
<div class="clu-kv"><span class="k">Aussteller</span><span class="v">Let's Encrypt R3</span></div>
<div class="clu-kv"><span class="k">Signatur</span><span class="v">ECDSA P-256</span></div>
<div class="clu-kv"><span class="k">Schlüsselgröße</span><span class="v">256 bit</span></div>
<div class="clu-kv"><span class="k">Ausgestellt am</span><span class="v">2026-03-22</span></div>
<div class="clu-kv"><span class="k">Läuft ab am</span><span class="v" style="color:var(--color-success);">2026-06-20</span></div>
<div class="clu-kv" style="border-bottom:0;"><span class="k">SSL-Labs Rating</span><span class="v" style="color:var(--color-success);">A+</span></div>
</div>
</div>
<div class="clu-card">
<div class="clu-card-head"><h3>Bisherige Zertifikate</h3></div>
<div class="clu-card-body tight">
<table class="clu-table">
<thead><tr><th>Ausgestellt</th><th>Abgelaufen</th><th>Status</th></tr></thead>
<tbody>
<tr><td class="font-mono">2026-03-22</td><td class="font-mono">2026-06-20</td><td><span class="clu-health">aktuell</span></td></tr>
<tr><td class="font-mono">2025-12-21</td><td class="font-mono">2026-03-21</td><td><span class="clu-health warn">ersetzt</span></td></tr>
<tr><td class="font-mono">2025-09-22</td><td class="font-mono">2025-12-21</td><td><span class="clu-health warn">ersetzt</span></td></tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
@endif
{{-- ────── SSH / FTP ────── --}}
@if ($tab === 'access')
<div class="clu-pane">
{{-- SSH-Users --}}
<div class="clu-card">
<div class="clu-card-head">
<h3 class="flex items-center gap-[8px]">
<svg class="clu-hico" width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M4 17l5-5-5-5M13 19h7"/></svg>
SSH-Benutzer
</h3>
<span class="meta">key-only · password-auth aus</span>
</div>
<div class="clu-card-body tight" style="overflow-x:auto;">
<table class="clu-table">
<thead><tr><th>Username</th><th>Public Key</th><th>Shell</th><th>Letzter Login</th><th>Scope</th><th></th></tr></thead>
<tbody>
<tr><td class="font-semibold">deploy</td><td class="font-mono text-[11px] text-(--color-muted)" style="max-width:280px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">ssh-ed25519 AAAAC3NzaC1l…7Q deploy@bergmann</td><td class="font-mono">/bin/bash</td><td class="font-mono">vor 14 min</td><td><span class="clu-pill accent">/var/www/bergmann</span></td><td><div class="clu-row-actions"><button type="button" title="Key tauschen"><svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 12a9 9 0 1 1-3-6.7M21 4v5h-5"/></svg></button><button type="button" title="Löschen"><svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 6h18M8 6V4h8v2M19 6l-1 14H6L5 6"/></svg></button></div></td></tr>
<tr><td class="font-semibold">m.weber</td><td class="font-mono text-[11px] text-(--color-muted)" style="max-width:280px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">ssh-ed25519 AAAAC3NzaC1l…1F m.weber@laptop</td><td class="font-mono">/bin/bash</td><td class="font-mono">heute, 09:42</td><td><span class="clu-pill accent">/var/www/bergmann</span></td><td><div class="clu-row-actions"><button type="button" title="Key tauschen"><svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 12a9 9 0 1 1-3-6.7M21 4v5h-5"/></svg></button><button type="button" title="Löschen"><svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 6h18M8 6V4h8v2M19 6l-1 14H6L5 6"/></svg></button></div></td></tr>
<tr><td class="font-semibold">ci-bot</td><td class="font-mono text-[11px] text-(--color-muted)" style="max-width:280px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">ssh-rsa AAAAB3NzaC1y…XY github-actions</td><td class="font-mono">/usr/bin/rrsync</td><td class="font-mono">vor 2h</td><td><span class="clu-pill muted">deploy only</span></td><td><div class="clu-row-actions"><button type="button" title="Key tauschen"><svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 12a9 9 0 1 1-3-6.7M21 4v5h-5"/></svg></button><button type="button" title="Löschen"><svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 6h18M8 6V4h8v2M19 6l-1 14H6L5 6"/></svg></button></div></td></tr>
</tbody>
</table>
</div>
<div class="clu-form-actions">
<button class="clu-ghost-btn" type="button" wire:click="notify('SSH Public Key importiert')">Public Key importieren</button>
<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>
SSH-Benutzer anlegen
</button>
</div>
</div>
{{-- SFTP / FTP-Users --}}
<div class="clu-card">
<div class="clu-card-head">
<h3>SFTP / FTP-Benutzer</h3>
<span class="meta">chroot · explicit-tls erzwungen</span>
</div>
<div class="clu-card-body tight" style="overflow-x:auto;">
<table class="clu-table">
<thead><tr><th>Username</th><th>Chroot</th><th>Protokoll</th><th>Quota</th><th>Letzter Upload</th><th></th></tr></thead>
<tbody>
<tr><td class="font-semibold">redakteur</td><td class="font-mono">/uploads/2026</td><td><span class="clu-pill success">SFTP</span></td><td class="font-mono">2.4 / 5 GB</td><td class="font-mono">heute, 11:02</td><td><div class="clu-row-actions"><button type="button" title="Passwort"><svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="5" y="11" width="14" height="9" rx="2"/><path d="M8 11V7a4 4 0 0 1 8 0v4"/></svg></button><button type="button" title="Löschen"><svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 6h18M8 6V4h8v2M19 6l-1 14H6L5 6"/></svg></button></div></td></tr>
<tr><td class="font-semibold">backup-puller</td><td class="font-mono">/backups</td><td><span class="clu-pill success">SFTP</span></td><td class="font-mono">128 GB</td><td class="font-mono">heute, 03:00</td><td><div class="clu-row-actions"><button type="button" title="Passwort"><svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="5" y="11" width="14" height="9" rx="2"/><path d="M8 11V7a4 4 0 0 1 8 0v4"/></svg></button><button type="button" title="Löschen"><svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 6h18M8 6V4h8v2M19 6l-1 14H6L5 6"/></svg></button></div></td></tr>
</tbody>
</table>
</div>
<div class="clu-form-actions">
<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>
SFTP-Benutzer anlegen
</button>
</div>
</div>
{{-- Security policy --}}
<div class="clu-card">
<div class="clu-card-head"><h3>Zugriffs-Policy</h3></div>
<div class="clu-card-body">
<div class="clu-toggle-row" wire:click="toggleSetting('access.password_auth')" style="cursor:pointer;">
<div class="clu-tr-body"><div class="clu-tr-title">Password-Auth deaktiviert</div><div class="clu-tr-desc">SSH nur über Public-Key zulassen</div></div>
<span class="clu-toggle @if($this->isOn('access.password_auth')) on @endif"></span>
</div>
<div class="clu-toggle-row" wire:click="toggleSetting('access.no_root')" style="cursor:pointer;">
<div class="clu-tr-body"><div class="clu-tr-title">Root-Login verbieten</div><div class="clu-tr-desc">PermitRootLogin no</div></div>
<span class="clu-toggle @if($this->isOn('access.no_root')) on @endif"></span>
</div>
<div class="clu-toggle-row" wire:click="toggleSetting('access.fail2ban')" style="cursor:pointer;">
<div class="clu-tr-body"><div class="clu-tr-title">fail2ban (3 Versuche → 1h Ban)</div><div class="clu-tr-desc">automatische IP-Sperre nach fehlversuchen</div></div>
<span class="clu-toggle @if($this->isOn('access.fail2ban')) on @endif"></span>
</div>
<div class="clu-toggle-row" wire:click="toggleSetting('access.ip_allowlist')" style="cursor:pointer;">
<div class="clu-tr-body"><div class="clu-tr-title">IP-Allowlist</div><div class="clu-tr-desc">SSH nur aus 10.10.0.0/16 + büro-WAN</div></div>
<span class="clu-toggle @if($this->isOn('access.ip_allowlist')) on @endif"></span>
</div>
<div class="clu-toggle-row" wire:click="toggleSetting('access.ssh_2fa')" style="cursor:pointer;">
<div class="clu-tr-body"><div class="clu-tr-title">2FA für SSH (Google Authenticator)</div><div class="clu-tr-desc">zusätzlich zum public key</div></div>
<span class="clu-toggle @if($this->isOn('access.ssh_2fa')) on @endif"></span>
</div>
</div>
<div class="clu-form-actions">
<button class="clu-btn-primary" type="button" style="height:36px;padding:0 14px;" wire:click="notify('Zugriffs-Policy gespeichert')">Policy speichern</button>
</div>
</div>
</div>
@endif
{{-- ────── WP + PHP (kompakte Hardcoded-Konfig nach Template) ────── --}}
@if ($tab === 'wp')
<div class="clu-pane">
{{-- wp-config.php card --}}
<div class="clu-card">
<div class="clu-card-head">
<h3 class="flex items-center gap-[8px]">
<svg class="clu-hico" width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M4 7h16M4 12h16M4 17h10"/></svg>
wp-config.php
</h3>
<span class="meta">/htdocs/wp-config.php</span>
</div>
<div class="clu-card-body">
<div class="clu-set-grid">
<div class="clu-field"><label>Tabellen-Präfix</label><div class="clu-input"><input value="wp_bg_" style="font-family:var(--font-mono);" type="text" /></div></div>
<div class="clu-field"><label>WP_MEMORY_LIMIT</label><div class="clu-input-group"><input value="256" type="text" /><span class="suffix">MB</span></div></div>
<div class="clu-field"><label>WP_MAX_MEMORY_LIMIT</label><div class="clu-input-group"><input value="512" type="text" /><span class="suffix">MB</span></div></div>
<div class="clu-field"><label>AUTOSAVE_INTERVAL</label><div class="clu-input-group"><input value="60" type="text" /><span class="suffix">sek</span></div></div>
<div class="clu-field"><label>WP_POST_REVISIONS</label><div class="clu-input"><input value="10" style="font-family:var(--font-mono);" type="text" /></div></div>
<div class="clu-field"><label>Sprache (WPLANG)</label><div class="clu-input"><input value="de_DE" style="font-family:var(--font-mono);" type="text" /></div></div>
</div>
<div style="margin-top:8px;">
<div class="clu-toggle-row" wire:click="toggleSetting('wp.debug')" style="cursor:pointer;"><div class="clu-tr-body"><div class="clu-tr-title">WP_DEBUG</div><div class="clu-tr-desc">debug-modus &amp; fehlerprotokoll</div></div><span class="clu-toggle @if($this->isOn('wp.debug')) on @endif"></span></div>
<div class="clu-toggle-row" wire:click="toggleSetting('wp.cache')" style="cursor:pointer;"><div class="clu-tr-body"><div class="clu-tr-title">WP_CACHE</div><div class="clu-tr-desc">persistentes caching aktiv</div></div><span class="clu-toggle @if($this->isOn('wp.cache')) on @endif"></span></div>
<div class="clu-toggle-row" wire:click="toggleSetting('wp.disallow_file_edit')" style="cursor:pointer;"><div class="clu-tr-body"><div class="clu-tr-title">DISALLOW_FILE_EDIT</div><div class="clu-tr-desc">datei-editor im wp-admin sperren</div></div><span class="clu-toggle @if($this->isOn('wp.disallow_file_edit')) on @endif"></span></div>
<div class="clu-toggle-row" wire:click="toggleSetting('wp.auto_updater_off')" style="cursor:pointer;"><div class="clu-tr-body"><div class="clu-tr-title">AUTOMATIC_UPDATER_DISABLED</div><div class="clu-tr-desc">automatische core-updates deaktiviert</div></div><span class="clu-toggle @if($this->isOn('wp.auto_updater_off')) on @endif"></span></div>
<div class="clu-toggle-row" wire:click="toggleSetting('wp.force_ssl_admin')" style="cursor:pointer;"><div class="clu-tr-body"><div class="clu-tr-title">FORCE_SSL_ADMIN</div><div class="clu-tr-desc">wp-admin nur über https</div></div><span class="clu-toggle @if($this->isOn('wp.force_ssl_admin')) on @endif"></span></div>
</div>
</div>
<div class="clu-form-actions">
<button class="clu-ghost-btn" type="button" wire:click="notify('wp-config.php im Editor geöffnet')">In Editor öffnen</button>
<button class="clu-btn-primary" type="button" style="height:36px;padding:0 14px;" wire:click="notify('wp-config.php gespeichert + nginx reload')">wp-config speichern</button>
</div>
</div>
{{-- Datenbank + Salts side-by-side --}}
<div class="grid gap-[14px]" style="grid-template-columns:1fr 1fr;">
<div class="clu-card">
<div class="clu-card-head"><h3>Datenbank</h3><span class="clu-pill success">verbunden</span></div>
<div class="clu-card-body">
<div class="clu-field"><label>DB_NAME</label><div class="clu-input"><input value="bergmann_prod" style="font-family:var(--font-mono);" type="text" /></div></div>
<div class="clu-field"><label>DB_USER</label><div class="clu-input"><input value="bergmann_wp" style="font-family:var(--font-mono);" type="text" /></div></div>
<div class="clu-field"><label>DB_HOST</label><div class="clu-input"><input value="127.0.0.1:3306" style="font-family:var(--font-mono);" type="text" /></div></div>
<div class="clu-field">
<label>DB_PASSWORD</label>
<div class="clu-input-group">
<input value="hunter2-very-secret" type="{{ $showDbPassword ? 'text' : 'password' }}" />
<span class="suffix" style="cursor:pointer;" wire:click="$toggle('showDbPassword')">
{{ $showDbPassword ? 'verbergen' : 'anzeigen' }}
</span>
</div>
</div>
</div>
</div>
<div class="clu-card">
<div class="clu-card-head"><h3>Sicherheits-Schlüssel (Salts)</h3></div>
<div class="clu-card-body">
<p class="clu-tr-desc" style="margin:0 0 14px;">AUTH_KEY, SECURE_AUTH_KEY, LOGGED_IN_KEY, NONCE_KEY &amp; zugehörige Salts. Neu generieren meldet alle Benutzer ab.</p>
<div class="clu-kv"><span class="k">Zuletzt rotiert</span><span class="v">14.03.2026</span></div>
<div class="clu-kv"><span class="k">Schlüssel-Länge</span><span class="v">64 zeichen</span></div>
<div class="clu-kv" style="border-bottom:0;"><span class="k">Status</span><span class="v" style="color:var(--color-success);">stark</span></div>
</div>
<div class="clu-form-actions">
<button class="clu-ghost-btn" type="button" style="color:var(--color-warning);border-color:rgba(194,130,15,0.35);" wire:click="notify('Salts neu generiert')">Salts neu generieren</button>
</div>
</div>
</div>
{{-- Allgemeine WP-Einstellungen --}}
<div class="clu-card">
<div class="clu-card-head"><h3>Allgemeine WordPress-Einstellungen</h3></div>
<div class="clu-card-body">
<div class="clu-set-grid">
<div class="clu-field"><label>Seitentitel</label><div class="clu-input"><input value="Bergmann Coffee Shop" type="text" /></div></div>
<div class="clu-field"><label>Untertitel</label><div class="clu-input"><input value="Rösterei &amp; Onlineshop" type="text" /></div></div>
<div class="clu-field"><label>WordPress-Adresse (URL)</label><div class="clu-input"><input value="https://{{ $site->domain }}" style="font-family:var(--font-mono);" type="text" /></div></div>
<div class="clu-field"><label>Permalink-Struktur</label><div class="clu-input"><input value="/%postname%/" style="font-family:var(--font-mono);" type="text" /></div></div>
</div>
<div style="margin-top:8px;">
<div class="clu-toggle-row" wire:click="toggleSetting('wp.search_visible')" style="cursor:pointer;"><div class="clu-tr-body"><div class="clu-tr-title">Suchmaschinen-Sichtbarkeit</div><div class="clu-tr-desc">von google &amp; co. indexieren lassen</div></div><span class="clu-toggle @if($this->isOn('wp.search_visible')) on @endif"></span></div>
<div class="clu-toggle-row" wire:click="toggleSetting('wp.registration')" style="cursor:pointer;"><div class="clu-tr-body"><div class="clu-tr-title">Registrierung erlauben</div><div class="clu-tr-desc">neue benutzer können sich registrieren</div></div><span class="clu-toggle @if($this->isOn('wp.registration')) on @endif"></span></div>
</div>
</div>
<div class="clu-form-actions">
<button class="clu-btn-primary" type="button" style="height:36px;padding:0 14px;" wire:click="notify('WordPress-Einstellungen gespeichert')">Einstellungen speichern</button>
</div>
</div>
</div>
@endif
@if ($tab === 'php')
<div class="clu-pane">
{{-- php.ini main card --}}
<div class="clu-card">
<div class="clu-card-head">
<h3 class="flex items-center gap-[8px]">
<svg class="clu-hico" width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M4 7h16M4 12h16M4 17h10"/></svg>
php.ini
</h3>
<span class="meta">php {{ $site->php_version }} · fpm pool: bergmann</span>
</div>
<div class="clu-card-body">
<div class="clu-set-grid">
<div class="clu-field"><label>memory_limit</label><div class="clu-input-group"><input value="256" type="text" /><span class="suffix">M</span></div></div>
<div class="clu-field"><label>max_execution_time</label><div class="clu-input-group"><input value="120" type="text" /><span class="suffix">sek</span></div></div>
<div class="clu-field"><label>upload_max_filesize</label><div class="clu-input-group"><input value="128" type="text" /><span class="suffix">M</span></div></div>
<div class="clu-field"><label>post_max_size</label><div class="clu-input-group"><input value="128" type="text" /><span class="suffix">M</span></div></div>
<div class="clu-field"><label>max_input_vars</label><div class="clu-input"><input value="3000" style="font-family:var(--font-mono);" type="text" /></div></div>
<div class="clu-field"><label>max_input_time</label><div class="clu-input-group"><input value="120" type="text" /><span class="suffix">sek</span></div></div>
<div class="clu-field"><label>default_charset</label><div class="clu-input"><input value="UTF-8" style="font-family:var(--font-mono);" type="text" /></div></div>
<div class="clu-field"><label>date.timezone</label><div class="clu-input"><input value="Europe/Berlin" style="font-family:var(--font-mono);" type="text" /></div></div>
</div>
<div style="margin-top:8px;">
<div class="clu-toggle-row" wire:click="toggleSetting('php.display_errors')" style="cursor:pointer;"><div class="clu-tr-body"><div class="clu-tr-title">display_errors</div><div class="clu-tr-desc">php-fehler im browser anzeigen (nur dev)</div></div><span class="clu-toggle @if($this->isOn('php.display_errors')) on @endif"></span></div>
<div class="clu-toggle-row" wire:click="toggleSetting('php.opcache_enable')" style="cursor:pointer;"><div class="clu-tr-body"><div class="clu-tr-title">opcache.enable</div><div class="clu-tr-desc">bytecode-cache aktiv</div></div><span class="clu-toggle @if($this->isOn('php.opcache_enable')) on @endif"></span></div>
<div class="clu-toggle-row" wire:click="toggleSetting('php.log_errors')" style="cursor:pointer;"><div class="clu-tr-body"><div class="clu-tr-title">log_errors</div><div class="clu-tr-desc">fehler in error.log schreiben</div></div><span class="clu-toggle @if($this->isOn('php.log_errors')) on @endif"></span></div>
<div class="clu-toggle-row" wire:click="toggleSetting('php.allow_url_fopen')" style="cursor:pointer;"><div class="clu-tr-body"><div class="clu-tr-title">allow_url_fopen</div><div class="clu-tr-desc">externe urls als datei öffnen</div></div><span class="clu-toggle @if($this->isOn('php.allow_url_fopen')) on @endif"></span></div>
</div>
</div>
<div class="clu-form-actions">
<button class="clu-ghost-btn" type="button" wire:click="notify('PHP-FPM-Pool neu gestartet')">PHP-FPM neu starten</button>
<button class="clu-btn-primary" type="button" style="height:36px;padding:0 14px;" wire:click="notify('php.ini gespeichert + FPM reload')">php.ini speichern</button>
</div>
</div>
{{-- OPcache + Extensions --}}
<div class="grid gap-[14px]" style="grid-template-columns:1fr 1fr;">
<div class="clu-card">
<div class="clu-card-head"><h3>OPcache</h3><span class="clu-pill success">aktiv</span></div>
<div class="clu-card-body">
<div class="clu-kv"><span class="k">Trefferquote</span><span class="v" style="color:var(--color-success);">99,2%</span></div>
<div class="clu-kv"><span class="k">Speicher</span><span class="v">142 / 256 MB</span></div>
<div class="clu-kv"><span class="k">Zwischengesp. Skripte</span><span class="v">4.218</span></div>
<div class="clu-kv" style="border-bottom:0;"><span class="k">opcache.revalidate_freq</span><span class="v">60 s</span></div>
</div>
<div class="clu-form-actions">
<button class="clu-ghost-btn" type="button" wire:click="notify('OPcache geleert')">OPcache leeren</button>
</div>
</div>
<div class="clu-card">
<div class="clu-card-head"><h3>Aktive Erweiterungen</h3><span class="meta">28 module</span></div>
<div class="clu-card-body">
<div class="flex flex-wrap gap-[8px]">
@foreach (['curl', 'gd', 'imagick', 'intl', 'mbstring', 'mysqli', 'opcache', 'redis', 'soap', 'zip', 'bcmath', 'exif'] as $ext)
<span class="clu-pill muted">{{ $ext }}</span>
@endforeach
<span class="clu-pill accent">+ 16</span>
</div>
</div>
</div>
</div>
</div>
@endif
</div>{{-- /clu-detail-content --}}
</div>{{-- /clu-detail-layout --}}
</div>