Site Details: exact template rebuild + xterm.js logs + fileContents tree
User-requested rebuild + browser-verified:
1. **Full template port** (templates/akutell/Site Details.html + sitedetails.js):
- All 6 tabs match template structure verbatim (Übersicht, WordPress,
PHP / ini, Datei-Manager, Cronjobs, Logs).
- Tab bar uses clu-dtabs glass button group instead of generic seg.
- Site hero (favicon + domain + server/wp/php/ssl meta) + Besuchen +
WP-Admin buttons in topbar.
2. **Filemanager tab** (template's clu-fm grid):
- File tree rendered recursively from PHP-mirrored sitedetails.js
'tree' array. Folder rows show clu-chev rotation on .open, nested
children indent via clu-tree-children border-left + margin-left.
- 7 baked file contents from sitedetails.js fileContents object
(wp-config.php, .htaccess, robots.txt, index.php, functions.php,
style.css, php.ini) — click any leaf in tree, editor loads it.
- clu-gutter dynamic line-numbers + clu-code-area textarea +
Verwerfen/Speichern action row.
3. **Logs tab with xterm.js**:
- resources/js/app.js imports @xterm/xterm + @xterm/addon-fit,
exposes window.XTerm + window.XTermFit globally.
- resources/css/app.css imports xterm.css.
- Sites show component pushes inline init script with logData baked
in (4 log types × ~8 entries each). xterm renders with ANSI
truecolor escapes per level (INFO blue / WARN yellow / ERROR red /
OK green / DEBUG purple), keeps dark gradient host with custom
scrollbar.
- 4-tab seg (access / error / php / wp) switches log type via
setLog() Livewire action; xterm re-init on livewire:navigated.
- Live folgen checkbox wired (Volt logFollow state).
4. **Cronjobs tab**:
- Real $site->crons table with name/expression-pill/command/last-run/
status + run/more actions + Cronjob-anlegen primary button.
5. **WordPress + PHP tabs**:
- Full forms with all 6 wp-config fields + 5 toggles + Datenbank +
Security Keys cards.
- php.ini with 8 fields + 4 toggles + OPcache stats + 16 extension
pills.
6. **CSS additions**: clu-dtabs/dtab, clu-pane, clu-site-hero, clu-fm,
clu-tree*, clu-chev, clu-editor*, clu-gutter, clu-code-area,
clu-xterm-host (dark gradient + custom scrollbar), clu-set-grid,
clu-switch-line, clu-section-head.
7. SiteDetailsTest assertions updated to match new template wording
(PHP / ini, Datei-Manager, Cronjobs, Schnellaktionen). 36 Pest tests
green.
8. Browser-verified: chart loads, xterm renders logs in correct color,
files tab tree expands + editor loads wp-config.php, 0 console errors.
master
parent
b605e0cd4f
commit
4d4e2cbc33
|
|
@ -5,9 +5,12 @@
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@xterm/addon-fit": "^0.11.0",
|
||||||
|
"@xterm/xterm": "^6.0.0",
|
||||||
"chart.js": "^4.5.1",
|
"chart.js": "^4.5.1",
|
||||||
"laravel-echo": "^2.3.4",
|
"laravel-echo": "^2.3.4",
|
||||||
"pusher-js": "^8.5.0"
|
"pusher-js": "^8.5.0",
|
||||||
|
"xterm": "^5.3.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tailwindcss/forms": "^0.5.2",
|
"@tailwindcss/forms": "^0.5.2",
|
||||||
|
|
@ -1168,6 +1171,21 @@
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@xterm/addon-fit": {
|
||||||
|
"version": "0.11.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@xterm/addon-fit/-/addon-fit-0.11.0.tgz",
|
||||||
|
"integrity": "sha512-jYcgT6xtVYhnhgxh3QgYDnnNMYTcf8ElbxxFzX0IZo+vabQqSPAjC3c1wJrKB5E19VwQei89QCiZZP86DCPF7g==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@xterm/xterm": {
|
||||||
|
"version": "6.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-6.0.0.tgz",
|
||||||
|
"integrity": "sha512-TQwDdQGtwwDt+2cgKDLn0IRaSxYu1tSUjgKarSDkUM0ZNiSRXFpjxEsvc/Zgc5kq5omJ+V0a8/kIM2WD3sMOYg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"workspaces": [
|
||||||
|
"addons/*"
|
||||||
|
]
|
||||||
|
},
|
||||||
"node_modules/agent-base": {
|
"node_modules/agent-base": {
|
||||||
"version": "6.0.2",
|
"version": "6.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
|
||||||
|
|
@ -2806,6 +2824,13 @@
|
||||||
"node": ">=0.4.0"
|
"node": ">=0.4.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/xterm": {
|
||||||
|
"version": "5.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/xterm/-/xterm-5.3.0.tgz",
|
||||||
|
"integrity": "sha512-8QqjlekLUFTrU6x7xck1MsPzPA571K5zNqWm0M0oroYEWVOptZ0+ubQSkQ3uxIEhcIHRujJy6emDWX4A7qyFzg==",
|
||||||
|
"deprecated": "This package is now deprecated. Move to @xterm/xterm instead.",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/y18n": {
|
"node_modules/y18n": {
|
||||||
"version": "5.0.8",
|
"version": "5.0.8",
|
||||||
"resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
|
"resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
|
||||||
|
|
|
||||||
|
|
@ -18,8 +18,11 @@
|
||||||
"vite": "^7.0.7"
|
"vite": "^7.0.7"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@xterm/addon-fit": "^0.11.0",
|
||||||
|
"@xterm/xterm": "^6.0.0",
|
||||||
"chart.js": "^4.5.1",
|
"chart.js": "^4.5.1",
|
||||||
"laravel-echo": "^2.3.4",
|
"laravel-echo": "^2.3.4",
|
||||||
"pusher-js": "^8.5.0"
|
"pusher-js": "^8.5.0",
|
||||||
|
"xterm": "^5.3.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
@import "tailwindcss";
|
@import "tailwindcss";
|
||||||
|
@import "@xterm/xterm/css/xterm.css";
|
||||||
|
|
||||||
/* Pull Tailwind utilities used by 3rd-party Blade templates so Tailwind v4
|
/* Pull Tailwind utilities used by 3rd-party Blade templates so Tailwind v4
|
||||||
generates them. Without these @source hints the scanner skips vendor/. */
|
generates them. Without these @source hints the scanner skips vendor/. */
|
||||||
|
|
@ -1203,6 +1204,152 @@
|
||||||
}
|
}
|
||||||
.clu-btn-danger:hover { filter: brightness(1.05); }
|
.clu-btn-danger:hover { filter: brightness(1.05); }
|
||||||
|
|
||||||
|
/* ───── Site Details — exact template port (dtabs / fm / editor / console) ───── */
|
||||||
|
.clu-dtabs {
|
||||||
|
display: flex; gap: 3px; padding: 5px;
|
||||||
|
border-radius: 15px;
|
||||||
|
overflow-x: auto;
|
||||||
|
background: var(--color-glass-strong);
|
||||||
|
backdrop-filter: blur(30px) saturate(180%);
|
||||||
|
-webkit-backdrop-filter: blur(30px) saturate(180%);
|
||||||
|
border: 1px solid var(--color-glass-border);
|
||||||
|
box-shadow: var(--shadow-glass), var(--shadow-glass-inset);
|
||||||
|
}
|
||||||
|
.clu-dtab {
|
||||||
|
display: flex; align-items: center; gap: 8px;
|
||||||
|
padding: 9px 14px;
|
||||||
|
border-radius: 10px;
|
||||||
|
color: #3a3a47;
|
||||||
|
font-weight: 600; font-size: 12.5px;
|
||||||
|
white-space: nowrap;
|
||||||
|
transition: background .15s, color .15s, box-shadow .15s;
|
||||||
|
}
|
||||||
|
.clu-dtab:hover { background: rgba(255,255,255,0.55); }
|
||||||
|
.clu-dtab.active {
|
||||||
|
color: var(--color-fg);
|
||||||
|
background: linear-gradient(180deg, #fff, rgba(255,255,255,0.8));
|
||||||
|
box-shadow: 0 4px 12px -5px rgba(40,52,92,0.3);
|
||||||
|
}
|
||||||
|
.clu-dtab .ico { width: 16px; height: 16px; color: var(--color-muted); }
|
||||||
|
.clu-dtab.active .ico { color: var(--color-accent); }
|
||||||
|
.clu-dtab .count {
|
||||||
|
font-family: var(--font-mono); font-size: 10px;
|
||||||
|
background: rgba(30,35,60,0.08);
|
||||||
|
padding: 1px 6px;
|
||||||
|
border-radius: 8px;
|
||||||
|
color: var(--color-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.clu-pane { display: flex; flex-direction: column; gap: 18px; animation: clu-fade .25s ease; }
|
||||||
|
|
||||||
|
.clu-site-hero { display: flex; align-items: center; gap: 14px; }
|
||||||
|
.clu-site-hero .clu-favicon { width: 42px; height: 42px; border-radius: 11px; font-size: 16px; }
|
||||||
|
.clu-site-hero h1 { margin: 0; font-size: 18px; font-weight: 600; letter-spacing: -0.02em; }
|
||||||
|
.clu-site-hero .sub {
|
||||||
|
font-family: var(--font-mono); font-size: 11.5px;
|
||||||
|
color: var(--color-muted);
|
||||||
|
margin-top: 3px;
|
||||||
|
display: flex; gap: 10px; flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* File-manager */
|
||||||
|
.clu-fm { display: grid; grid-template-columns: 268px 1fr; gap: 18px; align-items: start; }
|
||||||
|
@media (max-width: 1100px) { .clu-fm { grid-template-columns: 1fr; } }
|
||||||
|
.clu-tree { padding: 10px; max-height: 600px; overflow: auto; }
|
||||||
|
.clu-tree-head {
|
||||||
|
display: flex; align-items: center; justify-content: space-between;
|
||||||
|
padding: 6px 8px 10px;
|
||||||
|
border-bottom: 1px solid var(--color-hairline);
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
.clu-tree-head .path { font-family: var(--font-mono); font-size: 11px; color: var(--color-muted); }
|
||||||
|
.clu-tree-item {
|
||||||
|
display: flex; align-items: center; gap: 8px;
|
||||||
|
padding: 6px 9px;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 12.5px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
color: #3a3a47;
|
||||||
|
white-space: nowrap;
|
||||||
|
transition: background .12s;
|
||||||
|
}
|
||||||
|
.clu-tree-item:hover { background: rgba(255,255,255,0.6); }
|
||||||
|
.clu-tree-item.active { background: var(--color-accent-soft); color: var(--color-accent); }
|
||||||
|
.clu-tree-item .ti-ico { width: 15px; height: 15px; flex: none; color: var(--color-muted-2); }
|
||||||
|
.clu-tree-item.dir { font-weight: 600; color: var(--color-fg); }
|
||||||
|
.clu-tree-item.active .ti-ico { color: var(--color-accent); }
|
||||||
|
.clu-tree-children { margin-left: 9px; border-left: 1px solid var(--color-hairline); padding-left: 5px; }
|
||||||
|
.clu-tree-children.collapsed { display: none; }
|
||||||
|
.clu-chev { transition: transform .15s; color: var(--color-muted-2); display: inline-block; }
|
||||||
|
.clu-tree-item.open .clu-chev { transform: rotate(90deg); }
|
||||||
|
|
||||||
|
/* Code editor */
|
||||||
|
.clu-editor { display: flex; flex-direction: column; height: 600px; overflow: hidden; }
|
||||||
|
.clu-editor-head { display: flex; align-items: center; gap: 10px; padding: 12px 16px; border-bottom: 1px solid var(--color-hairline); }
|
||||||
|
.clu-crumb {
|
||||||
|
font-family: var(--font-mono); font-size: 12px;
|
||||||
|
color: var(--color-muted);
|
||||||
|
flex: 1; min-width: 0;
|
||||||
|
overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
|
||||||
|
}
|
||||||
|
.clu-crumb b { color: var(--color-fg); }
|
||||||
|
.clu-editor-tagline { font-family: var(--font-mono); font-size: 10.5px; color: var(--color-muted-2); }
|
||||||
|
.clu-code-wrap { flex: 1; display: flex; overflow: hidden; background: rgba(255,255,255,0.35); }
|
||||||
|
.clu-gutter {
|
||||||
|
padding: 14px 0;
|
||||||
|
text-align: right;
|
||||||
|
color: var(--color-muted-2);
|
||||||
|
background: rgba(30,35,60,0.04);
|
||||||
|
user-select: none;
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: 12px; line-height: 1.65;
|
||||||
|
border-right: 1px solid var(--color-hairline);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.clu-gutter div { padding: 0 11px; }
|
||||||
|
.clu-code-area {
|
||||||
|
flex: 1; border: 0; outline: 0; resize: none;
|
||||||
|
padding: 14px 16px;
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: 12.5px; line-height: 1.65;
|
||||||
|
background: transparent; color: var(--color-fg);
|
||||||
|
white-space: pre; overflow: auto;
|
||||||
|
tab-size: 4;
|
||||||
|
}
|
||||||
|
.clu-editor-foot { display: flex; align-items: center; justify-content: space-between; padding: 11px 16px; border-top: 1px solid var(--color-hairline); }
|
||||||
|
.clu-editor-empty {
|
||||||
|
flex: 1; display: grid; place-items: center;
|
||||||
|
color: var(--color-muted-2);
|
||||||
|
font-family: var(--font-mono); font-size: 12px;
|
||||||
|
text-align: center; gap: 10px; padding: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Console (logs) — xterm.js host */
|
||||||
|
.clu-xterm-host {
|
||||||
|
background: linear-gradient(180deg, rgba(24,26,42,0.96), rgba(17,19,32,0.96));
|
||||||
|
border: 1px solid rgba(255,255,255,0.09);
|
||||||
|
border-radius: var(--radius);
|
||||||
|
padding: 12px;
|
||||||
|
box-shadow: inset 0 1px 0 rgba(255,255,255,0.06), 0 10px 34px -10px rgba(40,52,92,0.3);
|
||||||
|
height: 540px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.clu-xterm-host .xterm { height: 100%; }
|
||||||
|
.clu-xterm-host .xterm-viewport::-webkit-scrollbar { width: 9px; }
|
||||||
|
.clu-xterm-host .xterm-viewport::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.14); border-radius: 5px; }
|
||||||
|
|
||||||
|
/* Two-column field grid (forms) */
|
||||||
|
.clu-set-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 0 26px; }
|
||||||
|
@media (max-width: 1100px) { .clu-set-grid { grid-template-columns: 1fr; } }
|
||||||
|
|
||||||
|
/* Switch line (toggle row) */
|
||||||
|
.clu-switch-line { display: flex; align-items: center; gap: 9px; font-size: 12px; color: var(--color-muted); font-weight: 500; cursor: pointer; }
|
||||||
|
|
||||||
|
/* Section head row */
|
||||||
|
.clu-section-head { display: flex; align-items: center; justify-content: space-between; gap: 12px; flex-wrap: wrap; }
|
||||||
|
.clu-section-head h3 { margin: 0; font-size: 14px; font-weight: 600; letter-spacing: -0.005em; }
|
||||||
|
|
||||||
/* File tree */
|
/* File tree */
|
||||||
.clu-file-tree {
|
.clu-file-tree {
|
||||||
font-family: var(--font-mono);
|
font-family: var(--font-mono);
|
||||||
|
|
|
||||||
|
|
@ -2,11 +2,14 @@ import './bootstrap';
|
||||||
import './login-effects';
|
import './login-effects';
|
||||||
import './register-effects';
|
import './register-effects';
|
||||||
import { Chart, registerables } from 'chart.js';
|
import { Chart, registerables } from 'chart.js';
|
||||||
|
import { Terminal } from '@xterm/xterm';
|
||||||
|
import { FitAddon } from '@xterm/addon-fit';
|
||||||
|
|
||||||
Chart.register(...registerables);
|
Chart.register(...registerables);
|
||||||
|
|
||||||
// Expose Chart globally so Blade inline <script> can create charts
|
// Expose for inline Blade scripts.
|
||||||
// without needing a separate import per page.
|
|
||||||
window.Chart = Chart;
|
window.Chart = Chart;
|
||||||
|
window.XTerm = Terminal;
|
||||||
|
window.XTermFit = FitAddon;
|
||||||
|
|
||||||
window.__clupilot = 1;
|
window.__clupilot = 1;
|
||||||
|
|
|
||||||
|
|
@ -11,115 +11,338 @@ new #[Layout('layouts.app')] class extends Component {
|
||||||
#[Url(as: 'tab')]
|
#[Url(as: 'tab')]
|
||||||
public string $tab = 'overview';
|
public string $tab = 'overview';
|
||||||
|
|
||||||
|
public string $activeFile = 'htdocs/wp-config.php';
|
||||||
|
public bool $logFollow = true;
|
||||||
|
public string $currentLog = 'access';
|
||||||
|
|
||||||
public function mount(Site $site): void
|
public function mount(Site $site): void
|
||||||
{
|
{
|
||||||
$this->site = $site->load(['server', 'certificate', 'crons', 'backups' => fn ($q) => $q->latest('completed_at')->limit(5)]);
|
$this->site = $site->load(['server', 'certificate', 'crons']);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function setTab(string $t): void
|
public function setTab(string $t): void { $this->tab = $t; }
|
||||||
{
|
|
||||||
$this->tab = $t;
|
public function setActiveFile(string $path): void { $this->activeFile = $path; }
|
||||||
}
|
|
||||||
|
public function setLog(string $type): void { $this->currentLog = $type; }
|
||||||
|
|
||||||
public function toggleMaintenance(): void
|
public function toggleMaintenance(): void
|
||||||
{
|
{
|
||||||
$this->site->maintenance_mode = ! $this->site->maintenance_mode;
|
$this->site->maintenance_mode = ! $this->site->maintenance_mode;
|
||||||
$this->site->save();
|
$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(); }
|
||||||
|
|
||||||
public function toggleCache(): void
|
/* File tree (nested) — verbatim from templates/akutell/sitedetails.js */
|
||||||
|
public function getFileTreeProperty(): array
|
||||||
{
|
{
|
||||||
$this->site->cache_enabled = ! $this->site->cache_enabled;
|
return [
|
||||||
$this->site->save();
|
[
|
||||||
|
'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'],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
public function toggleCdn(): void
|
/* fileContents from sitedetails.js verbatim */
|
||||||
|
public function getFileContentsProperty(): array
|
||||||
{
|
{
|
||||||
$this->site->cdn_enabled = ! $this->site->cdn_enabled;
|
return [
|
||||||
$this->site->save();
|
'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",
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
public function with(): array
|
/* Log entries per type (xterm renders these client-side) */
|
||||||
|
public function getLogDataProperty(): array
|
||||||
{
|
{
|
||||||
$logs = [
|
return [
|
||||||
['09:42:18', 'info', 'GET /wp-admin → 200 (74ms)'],
|
'access' => [
|
||||||
['09:42:21', 'info', 'POST /wp-admin/admin-ajax.php → 200 (118ms)'],
|
['09:12:04', 'INFO', '192.0.2.41 "GET /shop/" 200 18421 — 142ms'],
|
||||||
['09:42:24', 'warn', 'PHP Notice: Undefined index "session_id" in /wp-content/plugins/foo/bar.php:42'],
|
['09:12:05', 'INFO', '203.0.113.7 "GET /wp-json/wc/store/cart" 200 842 — 38ms'],
|
||||||
['09:42:26', 'info', 'Cache HIT /produkte/'],
|
['09:12:09', 'INFO', '192.0.2.41 "POST /?wc-ajax=add_to_cart" 200 311 — 96ms'],
|
||||||
['09:42:28', 'error', 'PHP Fatal error: Allowed memory size exhausted (tried to allocate 8192 bytes)'],
|
['09:12:14', 'INFO', '198.51.100.2 "GET /produkt/espresso-blend/" 200 21044 — 158ms'],
|
||||||
['09:42:30', 'info', 'WP cron: wp_scheduled_delete completed in 23ms'],
|
['09:12:21', 'WARN', '45.155.* "GET /wp-login.php" 403 — rate-limit'],
|
||||||
['09:42:33', 'warn', 'Slow query > 2s: SELECT * FROM wp_posts WHERE post_status="publish"'],
|
['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'],
|
||||||
|
],
|
||||||
];
|
];
|
||||||
|
}
|
||||||
|
|
||||||
$files = [
|
public function getCurrentFileContentProperty(): string
|
||||||
'📁 wp-content',
|
{
|
||||||
' 📁 plugins',
|
return $this->fileContents[$this->activeFile] ?? '';
|
||||||
' 📁 woocommerce',
|
|
||||||
' 📁 yoast-seo',
|
|
||||||
' 📁 themes',
|
|
||||||
' 📁 storefront',
|
|
||||||
' 📁 uploads',
|
|
||||||
'📁 wp-includes',
|
|
||||||
'📁 wp-admin',
|
|
||||||
'📄 wp-config.php',
|
|
||||||
'📄 .htaccess',
|
|
||||||
'📄 index.php',
|
|
||||||
];
|
|
||||||
|
|
||||||
return compact('logs', 'files');
|
|
||||||
}
|
}
|
||||||
}; ?>
|
}; ?>
|
||||||
|
|
||||||
|
@php
|
||||||
|
/** Recursive file tree renderer. */
|
||||||
|
if (! function_exists('clu_render_tree')) {
|
||||||
|
function clu_render_tree(array $items, string $activeFile): string {
|
||||||
|
$out = '';
|
||||||
|
foreach ($items as $node) {
|
||||||
|
$isDir = ($node['type'] ?? '') === 'dir';
|
||||||
|
$isOpen = $node['open'] ?? false;
|
||||||
|
$isActive = ! $isDir && ($node['path'] ?? null) === $activeFile;
|
||||||
|
$rowClass = 'clu-tree-item' . ($isDir ? ' dir' : '') . ($isOpen ? ' open' : '') . ($isActive ? ' active' : '');
|
||||||
|
$click = $isDir ? '' : 'wire:click="setActiveFile(\''.addslashes($node['path']).'\')" wire:navigate.hover';
|
||||||
|
$ico = $isDir
|
||||||
|
? '<svg class="ti-ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9"><path d="M3 7a2 2 0 0 1 2-2h4l2 2h8a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/></svg>'
|
||||||
|
: '<svg class="ti-ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><path d="M14 2v6h6"/></svg>';
|
||||||
|
$chev = $isDir
|
||||||
|
? '<svg class="clu-chev" width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M9 6l6 6-6 6"/></svg>'
|
||||||
|
: '<span style="width:11px;flex:none;"></span>';
|
||||||
|
$out .= '<div class="'.$rowClass.'" '.$click.'>'.$chev.$ico.'<span>'.htmlspecialchars($node['name']).'</span></div>';
|
||||||
|
if ($isDir && ! empty($node['children'])) {
|
||||||
|
$hidden = $isOpen ? '' : ' collapsed';
|
||||||
|
$out .= '<div class="clu-tree-children'.$hidden.'">'.clu_render_tree($node['children'], $activeFile).'</div>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $out;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@endphp
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
|
{{-- ── Topbar ── --}}
|
||||||
<div class="clu-topbar">
|
<div class="clu-topbar">
|
||||||
<x-clu.burger />
|
<x-clu.burger />
|
||||||
<div class="flex items-center gap-[12px]">
|
|
||||||
<a href="{{ route('sites.index') }}" wire:navigate class="clu-icon-btn" aria-label="Zurück">
|
<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="M19 12H5M12 5l-7 7 7 7"/></svg>
|
<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>
|
</a>
|
||||||
|
<div class="clu-site-hero">
|
||||||
|
<div class="clu-favicon" style="background:#15a06a;">{{ mb_strtoupper(mb_substr($site->domain, 0, 1)) }}</div>
|
||||||
<div>
|
<div>
|
||||||
<h1 class="clu-page-title">{{ $site->domain }}</h1>
|
<h1>{{ $site->domain }}</h1>
|
||||||
<div class="clu-page-sub">{{ $site->server->name }} · WP {{ $site->wp_version }} · PHP {{ $site->php_version }}</div>
|
<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>
|
</div>
|
||||||
<span class="clu-status-chip"><span class="clu-pulse-dot"></span> {{ ucfirst($site->status) }}</span>
|
</div>
|
||||||
|
<span class="clu-status-chip"><span class="clu-pulse-dot"></span> online</span>
|
||||||
<div class="spacer"></div>
|
<div class="spacer"></div>
|
||||||
<a href="https://{{ $site->domain }}" target="_blank" rel="noopener" class="clu-ghost-btn">
|
<a href="https://{{ $site->domain }}" target="_blank" rel="noopener" class="clu-ghost-btn">
|
||||||
Live ansehen
|
<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>
|
||||||
<svg width="12" height="12" 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>
|
</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>
|
</div>
|
||||||
|
|
||||||
{{-- Tab bar --}}
|
{{-- ── Tab bar ── --}}
|
||||||
<div class="clu-detail-tabs mb-[18px]" role="tablist">
|
<div class="clu-dtabs mb-[18px]" role="tablist">
|
||||||
@foreach (['overview' => 'Übersicht', 'wordpress' => 'WordPress', 'php' => 'PHP', 'files' => 'Files', 'cron' => 'Cron', 'logs' => 'Logs'] as $key => $label)
|
@foreach (['overview' => ['Übersicht', '<rect x="3" y="3" width="7" height="7" rx="1.5"/><rect x="14" y="3" width="7" height="7" rx="1.5"/><rect x="3" y="14" width="7" height="7" rx="1.5"/><rect x="14" y="14" width="7" height="7" rx="1.5"/>'], 'wp' => ['WordPress', '<circle cx="12" cy="12" r="9"/><path d="M3 12h18M12 3a14 14 0 0 1 0 18M12 3a14 14 0 0 0 0 18"/>'], 'php' => ['PHP / ini', '<path d="M4 7h16M4 12h16M4 17h10"/>'], 'files' => ['Datei-Manager', '<path d="M3 7a2 2 0 0 1 2-2h4l2 2h8a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/>'], 'cron' => ['Cronjobs', '<circle cx="12" cy="12" r="9"/><path d="M12 7v5l3 2"/>'], 'logs' => ['Logs', '<rect x="3" y="3" width="18" height="18" rx="2"/><path d="M7 8h10M7 12h10M7 16h6"/>']] as $key => [$label, $svg])
|
||||||
<button type="button" role="tab"
|
<button type="button" role="tab"
|
||||||
wire:click="setTab('{{ $key }}')"
|
wire:click="setTab('{{ $key }}')"
|
||||||
class="@if($tab === $key) active @endif">{{ $label }}</button>
|
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
|
@endforeach
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{{-- Tab content --}}
|
{{-- ────── OVERVIEW ────── --}}
|
||||||
@if ($tab === 'overview')
|
@if ($tab === 'overview')
|
||||||
<div class="grid gap-[14px] mb-[18px]" style="grid-template-columns:repeat(auto-fit,minmax(220px,1fr));">
|
<div class="clu-pane">
|
||||||
<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>
|
<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">Response</span></div><div class="clu-metric-value">{{ $site->response_ms }}<span class="unit">ms</span></div></div>
|
<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 @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>
|
<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"><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>
|
<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>
|
||||||
|
|
||||||
<div class="grid gap-[14px]" style="grid-template-columns:1fr 1fr;">
|
<div class="grid gap-[14px]" style="grid-template-columns:1fr 1fr;">
|
||||||
<div class="clu-card">
|
<div class="clu-card">
|
||||||
<div class="clu-card-head"><h3>Installation</h3></div>
|
<div class="clu-card-head"><h3>Installation</h3></div>
|
||||||
<div class="clu-card-body" style="display:flex;flex-direction:column;gap:8px;">
|
<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)">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 }}</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)">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)">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)">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>
|
</div>
|
||||||
<div class="clu-card">
|
<div class="clu-card">
|
||||||
<div class="clu-card-head"><h3>Quick Actions</h3></div>
|
<div class="clu-card-head"><h3>Schnellaktionen</h3></div>
|
||||||
<div class="clu-card-body" style="display:flex;flex-direction:column;gap:12px;">
|
<div class="clu-card-body" style="display:flex;flex-direction:column;gap:12px;">
|
||||||
<label class="clu-checkbox" style="justify-content:space-between;width:100%;">
|
<label class="clu-checkbox" style="justify-content:space-between;width:100%;">
|
||||||
<span>Maintenance Mode</span>
|
<span>Maintenance Mode</span>
|
||||||
|
|
@ -136,124 +359,224 @@ new #[Layout('layouts.app')] class extends Component {
|
||||||
<input type="checkbox" wire:click="toggleCdn" @checked($site->cdn_enabled) />
|
<input type="checkbox" wire:click="toggleCdn" @checked($site->cdn_enabled) />
|
||||||
<span class="box"><svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3.5"><path d="M5 12l5 5L20 7"/></svg></span>
|
<span class="box"><svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3.5"><path d="M5 12l5 5L20 7"/></svg></span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
<label class="clu-checkbox" style="justify-content:space-between;width:100%;">
|
||||||
</div>
|
<span>HTTPS erzwingen</span>
|
||||||
</div>
|
<input type="checkbox" @checked($site->https_enabled) />
|
||||||
@elseif ($tab === 'wordpress')
|
<span class="box"><svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3.5"><path d="M5 12l5 5L20 7"/></svg></span>
|
||||||
<div class="clu-card">
|
</label>
|
||||||
<div class="clu-card-head"><h3>WordPress Settings</h3></div>
|
|
||||||
<div class="clu-card-body" style="display:flex;flex-direction:column;gap:14px;">
|
|
||||||
<div class="clu-field-row">
|
|
||||||
<div class="flex flex-col gap-[6px]">
|
|
||||||
<label class="text-[11.5px] text-(--color-muted) font-semibold">Site URL</label>
|
|
||||||
<div class="clu-input"><input value="https://{{ $site->domain }}" type="text" /></div>
|
|
||||||
</div>
|
|
||||||
<div class="flex flex-col gap-[6px]">
|
|
||||||
<label class="text-[11.5px] text-(--color-muted) font-semibold">Admin Email</label>
|
|
||||||
<div class="clu-input"><input value="admin@{{ $site->domain }}" type="email" /></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="clu-field-row">
|
|
||||||
<div class="flex flex-col gap-[6px]">
|
|
||||||
<label class="text-[11.5px] text-(--color-muted) font-semibold">DB Name</label>
|
|
||||||
<div class="clu-input"><input value="wp_{{ str_replace('.', '_', $site->domain) }}" type="text" /></div>
|
|
||||||
</div>
|
|
||||||
<div class="flex flex-col gap-[6px]">
|
|
||||||
<label class="text-[11.5px] text-(--color-muted) font-semibold">DB Prefix</label>
|
|
||||||
<div class="clu-input"><input value="wp_" type="text" /></div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@elseif ($tab === 'php')
|
@endif
|
||||||
<div class="clu-card">
|
|
||||||
<div class="clu-card-head"><h3>PHP {{ $site->php_version }} · php.ini</h3></div>
|
{{-- ────── FILES ────── --}}
|
||||||
<div class="clu-card-body" style="display:flex;flex-direction:column;gap:14px;">
|
@if ($tab === 'files')
|
||||||
<div class="clu-field-row">
|
<div class="clu-pane">
|
||||||
<div class="flex flex-col gap-[6px]"><label class="text-[11.5px] text-(--color-muted) font-semibold">memory_limit</label><div class="clu-input"><input value="256M" type="text" /></div></div>
|
<div class="clu-section-head">
|
||||||
<div class="flex flex-col gap-[6px]"><label class="text-[11.5px] text-(--color-muted) font-semibold">max_execution_time</label><div class="clu-input"><input value="120" type="text" /></div></div>
|
<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-field-row">
|
|
||||||
<div class="flex flex-col gap-[6px]"><label class="text-[11.5px] text-(--color-muted) font-semibold">upload_max_filesize</label><div class="clu-input"><input value="64M" type="text" /></div></div>
|
|
||||||
<div class="flex flex-col gap-[6px]"><label class="text-[11.5px] text-(--color-muted) font-semibold">post_max_size</label><div class="clu-input"><input value="64M" type="text" /></div></div>
|
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div class="clu-fm">
|
||||||
<div class="clu-stat-label mb-2">Aktive Extensions</div>
|
<div class="clu-card clu-tree">
|
||||||
<div class="flex flex-wrap gap-[6px]">
|
<div class="clu-tree-head">
|
||||||
@foreach (['mysqli', 'gd', 'curl', 'mbstring', 'zip', 'xml', 'intl', 'imagick', 'redis', 'opcache'] as $ext)
|
<span class="path">/var/www/bergmann</span>
|
||||||
<span class="clu-pill muted">{{ $ext }}</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>
|
||||||
@endforeach
|
</div>
|
||||||
|
{!! clu_render_tree($this->fileTree, $activeFile) !!}
|
||||||
|
</div>
|
||||||
|
<div class="clu-card clu-editor" wire:key="editor-{{ $activeFile }}">
|
||||||
|
<div class="clu-editor-head">
|
||||||
|
<div class="clu-crumb">
|
||||||
|
{{ implode(' / ', array_slice(explode('/', $activeFile), 0, -1)) }} / <b>{{ basename($activeFile) }}</b>
|
||||||
|
</div>
|
||||||
|
<span class="clu-editor-tagline">{{ strtoupper(pathinfo($activeFile, PATHINFO_EXTENSION)) ?: 'TEXT' }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="clu-code-wrap">
|
||||||
|
@php $lines = max(1, substr_count($this->currentFileContent, "\n") + 1); @endphp
|
||||||
|
<div class="clu-gutter">@for ($i = 1; $i <= $lines; $i++)<div>{{ $i }}</div>@endfor</div>
|
||||||
|
<textarea class="clu-code-area" spellcheck="false" wire:ignore>{{ $this->currentFileContent }}</textarea>
|
||||||
|
</div>
|
||||||
|
<div class="clu-editor-foot">
|
||||||
|
<span class="text-[12px] font-mono text-(--color-muted)">✓ geladen · {{ $lines }} Zeilen</span>
|
||||||
|
<div class="flex gap-[8px]">
|
||||||
|
<button class="clu-ghost-btn" type="button">Verwerfen</button>
|
||||||
|
<button class="clu-btn-primary" type="button" style="height:36px;padding:0 16px;">Speichern</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@elseif ($tab === 'files')
|
|
||||||
<div class="grid gap-[14px]" style="grid-template-columns:280px 1fr;">
|
|
||||||
<div class="clu-card">
|
|
||||||
<div class="clu-card-head"><h3>Dateien</h3></div>
|
|
||||||
<div class="clu-card-body">
|
|
||||||
<div class="clu-file-tree">
|
|
||||||
@foreach ($files as $f)
|
|
||||||
<div class="clu-file-row">{{ $f }}</div>
|
|
||||||
@endforeach
|
|
||||||
</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>
|
||||||
<div class="clu-card">
|
<div class="clu-card">
|
||||||
<div class="clu-card-head"><h3>wp-config.php</h3><span class="meta">readonly preview</span></div>
|
<div class="clu-card-head"><h3>Geplante Aufgaben <span class="clu-pill muted">{{ $site->crons->count() }}</span></h3></div>
|
||||||
<div class="clu-card-body">
|
|
||||||
<pre class="clu-log-console" style="white-space:pre-wrap;"><?php /* keep template literal */ ?>
|
|
||||||
<?php
|
|
||||||
define('DB_NAME', 'wp_{{ str_replace('.', '_', $site->domain) }}');
|
|
||||||
define('DB_USER', 'wp_user');
|
|
||||||
define('DB_HOST', 'localhost');
|
|
||||||
define('WP_DEBUG', false);
|
|
||||||
define('WP_CACHE', {{ $site->cache_enabled ? 'true' : 'false' }});
|
|
||||||
$table_prefix = 'wp_';
|
|
||||||
require_once ABSPATH . 'wp-settings.php';</pre>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
@elseif ($tab === 'cron')
|
|
||||||
<div class="clu-card">
|
|
||||||
<div class="clu-card-head"><h3>Cronjobs</h3><span class="meta">{{ $site->crons->count() }} aktiv</span></div>
|
|
||||||
<div class="clu-card-body tight" style="overflow-x:auto;">
|
<div class="clu-card-body tight" style="overflow-x:auto;">
|
||||||
<table class="clu-table">
|
<table class="clu-table">
|
||||||
<thead><tr><th>Name</th><th>Expression</th><th>Command</th><th>Last Run</th><th>Status</th></tr></thead>
|
<thead><tr><th>Name</th><th>Zeitplan</th><th>Befehl</th><th>Letzter Lauf</th><th>Status</th><th></th></tr></thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@forelse ($site->crons as $c)
|
@forelse ($site->crons as $c)
|
||||||
<tr>
|
<tr>
|
||||||
<td class="font-semibold">{{ $c->name }}</td>
|
<td class="font-semibold">{{ $c->name }}</td>
|
||||||
<td class="font-mono">{{ $c->expression }}</td>
|
<td><span class="clu-pill accent">{{ $c->expression }}</span></td>
|
||||||
<td class="font-mono text-[11.5px] text-(--color-muted)">{{ $c->command }}</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 class="font-mono">{{ $c->last_run_at?->diffForHumans() ?? '—' }}</td>
|
||||||
<td>
|
<td>
|
||||||
@if ($c->status === 'ok')<span class="clu-health">ok</span>
|
@if ($c->status === 'ok')<span class="clu-health">ok</span>
|
||||||
@elseif ($c->status === 'failing')<span class="clu-health bad">failing</span>
|
@elseif ($c->status === 'failing')<span class="clu-health bad">failing</span>
|
||||||
@else <span class="clu-health warn">{{ $c->status }}</span>
|
@else<span class="clu-health warn">paused</span>@endif
|
||||||
@endif
|
|
||||||
</td>
|
</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>
|
</tr>
|
||||||
@empty
|
@empty
|
||||||
<tr><td colspan="5" style="text-align:center;padding:30px;color:var(--color-muted);">Keine Cronjobs.</td></tr>
|
<tr><td colspan="6" style="text-align:center;padding:30px;color:var(--color-muted);">Noch keine Cronjobs.</td></tr>
|
||||||
@endforelse
|
@endforelse
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@elseif ($tab === 'logs')
|
|
||||||
<div class="clu-card">
|
|
||||||
<div class="clu-card-head"><h3>Logs</h3><span class="meta">live tail · access / error / php</span></div>
|
|
||||||
<div class="clu-card-body">
|
|
||||||
<div class="clu-log-console">
|
|
||||||
@foreach ($logs as [$time, $level, $msg])
|
|
||||||
<div class="clu-log-line">
|
|
||||||
<span class="clu-log-time">{{ $time }}</span>
|
|
||||||
<span class="clu-log-level {{ $level }}">{{ strtoupper($level) }}</span>
|
|
||||||
<span class="clu-log-msg">{{ $msg }}</span>
|
|
||||||
</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
|
@endforeach
|
||||||
</div>
|
</div>
|
||||||
|
<label class="clu-switch-line">
|
||||||
|
<input type="checkbox" wire:model.live="logFollow" />
|
||||||
|
<span>Live folgen</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div wire:ignore wire:key="xterm-{{ $currentLog }}">
|
||||||
|
<div class="clu-xterm-host" id="clu-xterm-host" data-log-type="{{ $currentLog }}"></div>
|
||||||
|
</div>
|
||||||
|
@push('scripts')
|
||||||
|
<script>
|
||||||
|
(function () {
|
||||||
|
const logData = @json($this->logData);
|
||||||
|
function initXterm() {
|
||||||
|
const host = document.getElementById('clu-xterm-host');
|
||||||
|
if (!host) return;
|
||||||
|
if (!window.XTerm) { setTimeout(initXterm, 30); return; }
|
||||||
|
if (host._term) host._term.dispose();
|
||||||
|
|
||||||
|
const term = new window.XTerm({
|
||||||
|
convertEol: true,
|
||||||
|
cursorBlink: false,
|
||||||
|
disableStdin: true,
|
||||||
|
fontFamily: 'JetBrains Mono, ui-monospace, monospace',
|
||||||
|
fontSize: 12.5,
|
||||||
|
lineHeight: 1.4,
|
||||||
|
theme: {
|
||||||
|
background: 'rgba(0,0,0,0)',
|
||||||
|
foreground: '#cfd3e6',
|
||||||
|
cursor: '#5aa9ff',
|
||||||
|
selectionBackground: 'rgba(99,140,255,0.25)',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const fit = new window.XTermFit();
|
||||||
|
term.loadAddon(fit);
|
||||||
|
term.open(host);
|
||||||
|
try { fit.fit(); } catch (e) {}
|
||||||
|
|
||||||
|
const COLOR = { INFO: '\x1b[38;2;90;169;255m', WARN: '\x1b[38;2;231;185;78m', ERROR: '\x1b[38;2;255;115;115m', OK: '\x1b[38;2;76;214;160m', DEBUG: '\x1b[38;2;169;139;255m', TIME: '\x1b[38;2;107;113;145m', RESET: '\x1b[0m' };
|
||||||
|
const lines = logData[host.dataset.logType] || [];
|
||||||
|
lines.forEach(([t, lvl, msg]) => {
|
||||||
|
term.writeln(`${COLOR.TIME}${t}${COLOR.RESET} ${COLOR[lvl] || ''}${lvl.padEnd(5)}${COLOR.RESET} ${msg}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
host._term = term;
|
||||||
|
window.addEventListener('resize', () => { try { fit.fit(); } catch (e) {} });
|
||||||
|
}
|
||||||
|
initXterm();
|
||||||
|
document.addEventListener('livewire:navigated', initXterm);
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
@endpush
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
{{-- ────── WP + PHP (kompakte Hardcoded-Konfig nach Template) ────── --}}
|
||||||
|
@if ($tab === 'wp')
|
||||||
|
<div class="clu-pane">
|
||||||
|
<div class="clu-card">
|
||||||
|
<div class="clu-card-head"><h3>wp-config.php</h3><span class="meta">WordPress core</span></div>
|
||||||
|
<div class="clu-card-body" style="display:flex;flex-direction:column;gap:14px;">
|
||||||
|
<div class="clu-set-grid" style="row-gap:14px;">
|
||||||
|
@foreach (['Tabellen-Präfix' => 'wp_bg_', 'WP_MEMORY_LIMIT' => '256M', 'AUTOSAVE_INTERVAL' => '60', 'WP_POST_REVISIONS' => '10', 'WP_DEBUG_LOG' => '/var/www/bergmann/logs/wp-debug.log', 'WP_HOME' => 'https://shop.bergmann-coffee.com'] as $label => $val)
|
||||||
|
<div class="flex flex-col gap-[6px]"><label class="text-[11.5px] text-(--color-muted) font-semibold">{{ $label }}</label><div class="clu-input"><input value="{{ $val }}" type="text" /></div></div>
|
||||||
|
@endforeach
|
||||||
|
</div>
|
||||||
|
@foreach (['WP_DEBUG' => false, 'WP_CACHE' => true, 'DISALLOW_FILE_EDIT' => true, 'FORCE_SSL_ADMIN' => true, 'AUTOMATIC_UPDATER_DISABLED' => false] as $k => $v)
|
||||||
|
<label class="clu-checkbox" style="justify-content:space-between;width:100%;"><span>{{ $k }}</span><input type="checkbox" @checked($v) /><span class="box"><svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3.5"><path d="M5 12l5 5L20 7"/></svg></span></label>
|
||||||
|
@endforeach
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="grid gap-[14px]" style="grid-template-columns:1fr 1fr;">
|
||||||
|
<div class="clu-card"><div class="clu-card-head"><h3>Datenbank</h3></div><div class="clu-card-body" style="display:flex;flex-direction:column;gap:10px;">
|
||||||
|
@foreach (['DB_NAME' => 'bergmann_prod', 'DB_USER' => 'bergmann_wp', 'DB_HOST' => '127.0.0.1:3306', 'DB_CHARSET' => 'utf8mb4'] as $k => $v)
|
||||||
|
<div class="flex flex-col gap-[6px]"><label class="text-[11.5px] text-(--color-muted) font-semibold">{{ $k }}</label><div class="clu-input"><input value="{{ $v }}" type="text" /></div></div>
|
||||||
|
@endforeach
|
||||||
|
</div></div>
|
||||||
|
<div class="clu-card"><div class="clu-card-head"><h3>Security Keys</h3></div><div class="clu-card-body" style="display:flex;flex-direction:column;gap:10px;">
|
||||||
|
<div class="flex justify-between text-[12.5px]"><span class="text-(--color-muted)">AUTH_KEY</span><span class="font-mono">env</span></div>
|
||||||
|
<div class="flex justify-between text-[12.5px]"><span class="text-(--color-muted)">SECURE_AUTH_KEY</span><span class="font-mono">env</span></div>
|
||||||
|
<div class="flex justify-between text-[12.5px]"><span class="text-(--color-muted)">Rotation</span><span class="font-mono">vor 11 Wochen</span></div>
|
||||||
|
<button class="clu-btn-primary" type="button" style="height:34px;padding:0 14px;align-self:flex-start;">Schlüssel rotieren</button>
|
||||||
|
</div></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
@if ($tab === 'php')
|
||||||
|
<div class="clu-pane">
|
||||||
|
<div class="clu-card">
|
||||||
|
<div class="clu-card-head"><h3>php.ini · PHP {{ $site->php_version }}</h3></div>
|
||||||
|
<div class="clu-card-body" style="display:flex;flex-direction:column;gap:14px;">
|
||||||
|
<div class="clu-set-grid" style="row-gap:14px;">
|
||||||
|
@foreach (['memory_limit' => '256M', 'max_execution_time' => '60', 'max_input_time' => '60', 'upload_max_filesize' => '64M', 'post_max_size' => '64M', 'date.timezone' => 'Europe/Berlin', 'opcache.memory_consumption' => '256', 'opcache.max_accelerated_files' => '20000'] as $k => $v)
|
||||||
|
<div class="flex flex-col gap-[6px]"><label class="text-[11.5px] text-(--color-muted) font-semibold">{{ $k }}</label><div class="clu-input"><input value="{{ $v }}" type="text" /></div></div>
|
||||||
|
@endforeach
|
||||||
|
</div>
|
||||||
|
@foreach (['display_errors' => false, 'opcache.enable' => true, 'log_errors' => true, 'allow_url_fopen' => false] as $k => $v)
|
||||||
|
<label class="clu-checkbox" style="justify-content:space-between;width:100%;"><span>{{ $k }}</span><input type="checkbox" @checked($v) /><span class="box"><svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3.5"><path d="M5 12l5 5L20 7"/></svg></span></label>
|
||||||
|
@endforeach
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="grid gap-[14px]" style="grid-template-columns:1fr 1fr;">
|
||||||
|
<div class="clu-card"><div class="clu-card-head"><h3>OPcache</h3></div><div class="clu-card-body" style="display:flex;flex-direction:column;gap:9px;">
|
||||||
|
<div class="flex justify-between text-[12.5px]"><span class="text-(--color-muted)">Hit-Rate</span><span class="font-mono">99.2%</span></div>
|
||||||
|
<div class="flex justify-between text-[12.5px]"><span class="text-(--color-muted)">Scripts</span><span class="font-mono">4 218</span></div>
|
||||||
|
<div class="flex justify-between text-[12.5px]"><span class="text-(--color-muted)">Memory</span><span class="font-mono">187 / 256 MB</span></div>
|
||||||
|
<button class="clu-ghost-btn" type="button" style="align-self:flex-start;">Cache leeren</button>
|
||||||
|
</div></div>
|
||||||
|
<div class="clu-card"><div class="clu-card-head"><h3>Aktive Erweiterungen</h3></div><div class="clu-card-body"><div class="flex flex-wrap gap-[6px]">
|
||||||
|
@foreach (['curl', 'gd', 'imagick', 'mbstring', 'mysqli', 'opcache', 'redis', 'xml', 'zip', 'intl', 'json', 'soap', 'sodium', 'exif', 'fileinfo', 'iconv'] as $ext)<span class="clu-pill muted">{{ $ext }}</span>@endforeach
|
||||||
|
</div></div></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@endif
|
@endif
|
||||||
|
|
|
||||||
|
|
@ -29,12 +29,12 @@ it('renders the overview tab by default', function () {
|
||||||
->assertSeeText('detail-test.de')
|
->assertSeeText('detail-test.de')
|
||||||
->assertSeeText('Übersicht')
|
->assertSeeText('Übersicht')
|
||||||
->assertSeeText('WordPress')
|
->assertSeeText('WordPress')
|
||||||
->assertSeeText('PHP')
|
->assertSeeText('PHP / ini')
|
||||||
->assertSeeText('Files')
|
->assertSeeText('Datei-Manager')
|
||||||
->assertSeeText('Cron')
|
->assertSeeText('Cronjobs')
|
||||||
->assertSeeText('Logs')
|
->assertSeeText('Logs')
|
||||||
->assertSeeText('Installation')
|
->assertSeeText('Installation')
|
||||||
->assertSeeText('Quick Actions');
|
->assertSeeText('Schnellaktionen');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('switches between all six tabs', function () {
|
it('switches between all six tabs', function () {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue