1452 lines
102 KiB
PHP
1452 lines
102 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';
|
|
|
|
/** 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';
|
|
|
|
/** Generic settings bag for all toggle-rows across tabs. */
|
|
public array $settings = [
|
|
// vHost
|
|
'vhost.server_tokens' => true,
|
|
'vhost.http2_push' => false,
|
|
// 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>
|
|
{{-- ── 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"/>'], 'plugins' => ['Plugins', '<path d="M14 7V3H8v4M3 7h18v13H3zM12 11v6M9 14h6"/>'], 'redirects' => ['Redirects', '<path d="M3 12h14l-4-4M17 12l-4 4"/>'], 'staging' => ['Staging', '<path d="M3 12a9 9 0 1 0 9-9M3 4v5h5M21 12a9 9 0 1 0-9 9M21 20v-5h-5"/>'], 'vhost' => ['vHost', '<path d="M2 12h20M2 6h20M2 18h20"/>'], 'ssl' => ['SSL / TLS', '<rect x="5" y="11" width="14" height="9" rx="2"/><path d="M8 11V7a4 4 0 0 1 8 0v4"/>'], 'access' => ['SSH / FTP', '<path d="M4 17l5-5-5-5M13 19h7"/>'], '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">
|
|
<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"><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, $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">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
|
|
|
|
{{-- ────── 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">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;">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">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]">
|
|
<button type="button" class="clu-ghost-btn" style="background:var(--color-accent-soft);border-color:var(--color-accent);color:var(--color-accent);">301 (Permanent)</button>
|
|
<button type="button" class="clu-ghost-btn">302 (Temporär)</button>
|
|
<button type="button" class="clu-ghost-btn">307 (Strict)</button>
|
|
<button type="button" class="clu-ghost-btn">410 (Gone)</button>
|
|
</div>
|
|
</div>
|
|
<div class="clu-field">
|
|
<label>Match-Typ</label>
|
|
<select class="clu-input" style="appearance:auto;font-family:var(--font-mono);">
|
|
<option>Exact</option>
|
|
<option>Prefix</option>
|
|
<option>Regex</option>
|
|
<option>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">Aus CSV importieren</button>
|
|
<button class="clu-btn-primary" type="button" style="height:36px;padding:0 14px;">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);">Staging löschen</button>
|
|
@endif
|
|
<button class="clu-ghost-btn" type="button">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">nginx -t (Syntax-Check)</button>
|
|
<button class="clu-btn-primary" type="button" style="height:36px;padding:0 14px;">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">Cert herunterladen</button>
|
|
<button class="clu-ghost-btn" type="button">Jetzt erneuern</button>
|
|
<button class="clu-btn-primary" type="button" style="height:36px;padding:0 14px;">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">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;">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"><div class="clu-tr-body"><div class="clu-tr-title">WP_DEBUG</div><div class="clu-tr-desc">debug-modus & fehlerprotokoll</div></div><span class="clu-toggle"></span></div>
|
|
<div class="clu-toggle-row"><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 on"></span></div>
|
|
<div class="clu-toggle-row"><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 on"></span></div>
|
|
<div class="clu-toggle-row"><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"></span></div>
|
|
<div class="clu-toggle-row"><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 on"></span></div>
|
|
</div>
|
|
</div>
|
|
<div class="clu-form-actions">
|
|
<button class="clu-ghost-btn" type="button">In Editor öffnen</button>
|
|
<button class="clu-btn-primary" type="button" style="height:36px;padding:0 14px;">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="************" type="password" /><span class="suffix" style="cursor:pointer;">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 & 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);">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 & 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"><div class="clu-tr-body"><div class="clu-tr-title">Suchmaschinen-Sichtbarkeit</div><div class="clu-tr-desc">von google & co. indexieren lassen</div></div><span class="clu-toggle on"></span></div>
|
|
<div class="clu-toggle-row"><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"></span></div>
|
|
</div>
|
|
</div>
|
|
<div class="clu-form-actions">
|
|
<button class="clu-btn-primary" type="button" style="height:36px;padding:0 14px;">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"><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"></span></div>
|
|
<div class="clu-toggle-row"><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 on"></span></div>
|
|
<div class="clu-toggle-row"><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 on"></span></div>
|
|
<div class="clu-toggle-row"><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 on"></span></div>
|
|
</div>
|
|
</div>
|
|
<div class="clu-form-actions">
|
|
<button class="clu-ghost-btn" type="button">PHP-FPM neu starten</button>
|
|
<button class="clu-btn-primary" type="button" style="height:36px;padding:0 14px;">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">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>
|