Portainer-ready compose stack + app changes

master
Boban Blaskovic 2026-05-31 17:04:03 +02:00
parent 624b21fcb9
commit 67598ab649
34 changed files with 3777 additions and 1002 deletions

18
app/.editorconfig Normal file
View File

@ -0,0 +1,18 @@
root = true
[*]
charset = utf-8
end_of_line = lf
indent_size = 4
indent_style = space
insert_final_newline = true
trim_trailing_whitespace = true
[*.md]
trim_trailing_whitespace = false
[*.{yml,yaml}]
indent_size = 2
[compose.yaml]
indent_size = 4

59
app/README.md Normal file
View File

@ -0,0 +1,59 @@
<p align="center"><a href="https://laravel.com" target="_blank"><img src="https://raw.githubusercontent.com/laravel/art/master/logo-lockup/5%20SVG/2%20CMYK/1%20Full%20Color/laravel-logolockup-cmyk-red.svg" width="400" alt="Laravel Logo"></a></p>
<p align="center">
<a href="https://github.com/laravel/framework/actions"><img src="https://github.com/laravel/framework/workflows/tests/badge.svg" alt="Build Status"></a>
<a href="https://packagist.org/packages/laravel/framework"><img src="https://img.shields.io/packagist/dt/laravel/framework" alt="Total Downloads"></a>
<a href="https://packagist.org/packages/laravel/framework"><img src="https://img.shields.io/packagist/v/laravel/framework" alt="Latest Stable Version"></a>
<a href="https://packagist.org/packages/laravel/framework"><img src="https://img.shields.io/packagist/l/laravel/framework" alt="License"></a>
</p>
## About Laravel
Laravel is a web application framework with expressive, elegant syntax. We believe development must be an enjoyable and creative experience to be truly fulfilling. Laravel takes the pain out of development by easing common tasks used in many web projects, such as:
- [Simple, fast routing engine](https://laravel.com/docs/routing).
- [Powerful dependency injection container](https://laravel.com/docs/container).
- Multiple back-ends for [session](https://laravel.com/docs/session) and [cache](https://laravel.com/docs/cache) storage.
- Expressive, intuitive [database ORM](https://laravel.com/docs/eloquent).
- Database agnostic [schema migrations](https://laravel.com/docs/migrations).
- [Robust background job processing](https://laravel.com/docs/queues).
- [Real-time event broadcasting](https://laravel.com/docs/broadcasting).
Laravel is accessible, powerful, and provides tools required for large, robust applications.
## Learning Laravel
Laravel has the most extensive and thorough [documentation](https://laravel.com/docs) and video tutorial library of all modern web application frameworks, making it a breeze to get started with the framework. You can also check out [Laravel Learn](https://laravel.com/learn), where you will be guided through building a modern Laravel application.
If you don't feel like reading, [Laracasts](https://laracasts.com) can help. Laracasts contains thousands of video tutorials on a range of topics including Laravel, modern PHP, unit testing, and JavaScript. Boost your skills by digging into our comprehensive video library.
## Laravel Sponsors
We would like to extend our thanks to the following sponsors for funding Laravel development. If you are interested in becoming a sponsor, please visit the [Laravel Partners program](https://partners.laravel.com).
### Premium Partners
- **[Vehikl](https://vehikl.com)**
- **[Tighten Co.](https://tighten.co)**
- **[Kirschbaum Development Group](https://kirschbaumdevelopment.com)**
- **[64 Robots](https://64robots.com)**
- **[Curotec](https://www.curotec.com/services/technologies/laravel)**
- **[DevSquad](https://devsquad.com/hire-laravel-developers)**
- **[Redberry](https://redberry.international/laravel-development)**
- **[Active Logic](https://activelogic.com)**
## Contributing
Thank you for considering contributing to the Laravel framework! The contribution guide can be found in the [Laravel documentation](https://laravel.com/docs/contributions).
## Code of Conduct
In order to ensure that the Laravel community is welcoming to all, please review and abide by the [Code of Conduct](https://laravel.com/docs/contributions#code-of-conduct).
## Security Vulnerabilities
If you discover a security vulnerability within Laravel, please send an e-mail to Taylor Otwell via [taylor@laravel.com](mailto:taylor@laravel.com). All security vulnerabilities will be promptly addressed.
## License
The Laravel framework is open-sourced software licensed under the [MIT license](https://opensource.org/licenses/MIT).

View File

@ -1,27 +0,0 @@
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use Illuminate\Auth\Events\Verified;
use Illuminate\Foundation\Auth\EmailVerificationRequest;
use Illuminate\Http\RedirectResponse;
class VerifyEmailController extends Controller
{
/**
* Mark the authenticated user's email address as verified.
*/
public function __invoke(EmailVerificationRequest $request): RedirectResponse
{
if ($request->user()->hasVerifiedEmail()) {
return redirect()->intended(route('dashboard', absolute: false).'?verified=1');
}
if ($request->user()->markEmailAsVerified()) {
event(new Verified($request->user()));
}
return redirect()->intended(route('dashboard', absolute: false).'?verified=1');
}
}

View File

@ -21,5 +21,14 @@ class DatabaseSeeder extends Seeder
'name' => 'Test User',
'email' => 'test@example.com',
]);
User::updateOrCreate(
['email' => 'admin@clupilot.test'],
[
'name' => 'Admin',
'password' => bcrypt('clupilot'),
'email_verified_at' => now(),
]
);
}
}

810
app/package-lock.json generated
View File

@ -16,23 +16,10 @@
"concurrently": "^9.0.1",
"laravel-vite-plugin": "^2.0.0",
"postcss": "^8.4.31",
"tailwindcss": "^3.4.19",
"tailwindcss": "^4.3.0",
"vite": "^7.0.7"
}
},
"node_modules/@alloc/quick-lru": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz",
"integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/@esbuild/aix-ppc64": {
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz",
@ -525,44 +512,6 @@
"@jridgewell/sourcemap-codec": "^1.4.14"
}
},
"node_modules/@nodelib/fs.scandir": {
"version": "2.1.5",
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
"integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==",
"dev": true,
"license": "MIT",
"dependencies": {
"@nodelib/fs.stat": "2.0.5",
"run-parallel": "^1.1.9"
},
"engines": {
"node": ">= 8"
}
},
"node_modules/@nodelib/fs.stat": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz",
"integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 8"
}
},
"node_modules/@nodelib/fs.walk": {
"version": "1.2.8",
"resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz",
"integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@nodelib/fs.scandir": "2.1.5",
"fastq": "^1.6.0"
},
"engines": {
"node": ">= 8"
}
},
"node_modules/@rollup/rollup-android-arm-eabi": {
"version": "4.60.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.4.tgz",
@ -949,13 +898,6 @@
"tailwindcss": "4.3.0"
}
},
"node_modules/@tailwindcss/node/node_modules/tailwindcss": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.0.tgz",
"integrity": "sha512-y6nxMGB1nMW9R6k96e5gdIFzcfL/gTJRNaqGes1YvkLnPVXzWgbqFF2yLC0T8G774n24cx3Pe8XrKoniCOAH+Q==",
"dev": true,
"license": "MIT"
},
"node_modules/@tailwindcss/oxide": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.3.0.tgz",
@ -1212,13 +1154,6 @@
"vite": "^5.2.0 || ^6 || ^7 || ^8"
}
},
"node_modules/@tailwindcss/vite/node_modules/tailwindcss": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.0.tgz",
"integrity": "sha512-y6nxMGB1nMW9R6k96e5gdIFzcfL/gTJRNaqGes1YvkLnPVXzWgbqFF2yLC0T8G774n24cx3Pe8XrKoniCOAH+Q==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/estree": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
@ -1265,34 +1200,6 @@
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
"node_modules/any-promise": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz",
"integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==",
"dev": true,
"license": "MIT"
},
"node_modules/anymatch": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
"integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
"dev": true,
"license": "ISC",
"dependencies": {
"normalize-path": "^3.0.0",
"picomatch": "^2.0.4"
},
"engines": {
"node": ">= 8"
}
},
"node_modules/arg": {
"version": "5.0.2",
"resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz",
"integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==",
"dev": true,
"license": "MIT"
},
"node_modules/asynckit": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
@ -1363,32 +1270,6 @@
"node": ">=6.0.0"
}
},
"node_modules/binary-extensions": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
"integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/braces": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
"integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
"dev": true,
"license": "MIT",
"dependencies": {
"fill-range": "^7.1.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/browserslist": {
"version": "4.28.2",
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz",
@ -1437,16 +1318,6 @@
"node": ">= 0.4"
}
},
"node_modules/camelcase-css": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz",
"integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 6"
}
},
"node_modules/caniuse-lite": {
"version": "1.0.30001793",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001793.tgz",
@ -1498,44 +1369,6 @@
"node": ">=8"
}
},
"node_modules/chokidar": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
"integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
"dev": true,
"license": "MIT",
"dependencies": {
"anymatch": "~3.1.2",
"braces": "~3.0.2",
"glob-parent": "~5.1.2",
"is-binary-path": "~2.1.0",
"is-glob": "~4.0.1",
"normalize-path": "~3.0.0",
"readdirp": "~3.6.0"
},
"engines": {
"node": ">= 8.10.0"
},
"funding": {
"url": "https://paulmillr.com/funding/"
},
"optionalDependencies": {
"fsevents": "~2.3.2"
}
},
"node_modules/chokidar/node_modules/glob-parent": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
"integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
"dev": true,
"license": "ISC",
"dependencies": {
"is-glob": "^4.0.1"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/cliui": {
"version": "8.0.1",
"resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
@ -1584,16 +1417,6 @@
"node": ">= 0.8"
}
},
"node_modules/commander": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz",
"integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 6"
}
},
"node_modules/concurrently": {
"version": "9.2.1",
"resolved": "https://registry.npmjs.org/concurrently/-/concurrently-9.2.1.tgz",
@ -1619,19 +1442,6 @@
"url": "https://github.com/open-cli-tools/concurrently?sponsor=1"
}
},
"node_modules/cssesc": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
"integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==",
"dev": true,
"license": "MIT",
"bin": {
"cssesc": "bin/cssesc"
},
"engines": {
"node": ">=4"
}
},
"node_modules/debug": {
"version": "4.4.3",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
@ -1669,20 +1479,6 @@
"node": ">=8"
}
},
"node_modules/didyoumean": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz",
"integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==",
"dev": true,
"license": "Apache-2.0"
},
"node_modules/dlv": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz",
"integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==",
"dev": true,
"license": "MIT"
},
"node_modules/dunder-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
@ -1851,59 +1647,6 @@
"node": ">=6"
}
},
"node_modules/fast-glob": {
"version": "3.3.3",
"resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz",
"integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@nodelib/fs.stat": "^2.0.2",
"@nodelib/fs.walk": "^1.2.3",
"glob-parent": "^5.1.2",
"merge2": "^1.3.0",
"micromatch": "^4.0.8"
},
"engines": {
"node": ">=8.6.0"
}
},
"node_modules/fast-glob/node_modules/glob-parent": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
"integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
"dev": true,
"license": "ISC",
"dependencies": {
"is-glob": "^4.0.1"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/fastq": {
"version": "1.20.1",
"resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz",
"integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==",
"dev": true,
"license": "ISC",
"dependencies": {
"reusify": "^1.0.4"
}
},
"node_modules/fill-range": {
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
"integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
"dev": true,
"license": "MIT",
"dependencies": {
"to-regex-range": "^5.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/follow-redirects": {
"version": "1.16.0",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz",
@ -2030,19 +1773,6 @@
"node": ">= 0.4"
}
},
"node_modules/glob-parent": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
"integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
"dev": true,
"license": "ISC",
"dependencies": {
"is-glob": "^4.0.3"
},
"engines": {
"node": ">=10.13.0"
}
},
"node_modules/gopd": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
@ -2129,45 +1859,6 @@
"node": ">= 6"
}
},
"node_modules/is-binary-path": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
"integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
"dev": true,
"license": "MIT",
"dependencies": {
"binary-extensions": "^2.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/is-core-module": {
"version": "2.16.2",
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.2.tgz",
"integrity": "sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==",
"dev": true,
"license": "MIT",
"dependencies": {
"hasown": "^2.0.3"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/is-extglob": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
"integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/is-fullwidth-code-point": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
@ -2178,29 +1869,6 @@
"node": ">=8"
}
},
"node_modules/is-glob": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
"integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
"dev": true,
"license": "MIT",
"dependencies": {
"is-extglob": "^2.1.1"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/is-number": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.12.0"
}
},
"node_modules/jiti": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz",
@ -2505,26 +2173,6 @@
"url": "https://opencollective.com/parcel"
}
},
"node_modules/lilconfig": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz",
"integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=14"
},
"funding": {
"url": "https://github.com/sponsors/antonk52"
}
},
"node_modules/lines-and-columns": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
"integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==",
"dev": true,
"license": "MIT"
},
"node_modules/magic-string": {
"version": "0.30.21",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
@ -2545,30 +2193,6 @@
"node": ">= 0.4"
}
},
"node_modules/merge2": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
"integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 8"
}
},
"node_modules/micromatch": {
"version": "4.0.8",
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
"integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
"dev": true,
"license": "MIT",
"dependencies": {
"braces": "^3.0.3",
"picomatch": "^2.3.1"
},
"engines": {
"node": ">=8.6"
}
},
"node_modules/mime-db": {
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
@ -2608,18 +2232,6 @@
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"license": "MIT"
},
"node_modules/mz": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz",
"integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"any-promise": "^1.0.0",
"object-assign": "^4.0.1",
"thenify-all": "^1.0.0"
}
},
"node_modules/nanoid": {
"version": "3.3.12",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz",
@ -2649,43 +2261,6 @@
"node": ">=18"
}
},
"node_modules/normalize-path": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
"integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/object-assign": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
"integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/object-hash": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz",
"integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 6"
}
},
"node_modules/path-parse": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
"integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
"dev": true,
"license": "MIT"
},
"node_modules/picocolors": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
@ -2706,26 +2281,6 @@
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/pify": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
"integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/pirates": {
"version": "4.0.7",
"resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz",
"integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 6"
}
},
"node_modules/postcss": {
"version": "8.5.15",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz",
@ -2755,133 +2310,6 @@
"node": "^10 || ^12 || >=14"
}
},
"node_modules/postcss-import": {
"version": "15.1.0",
"resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz",
"integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==",
"dev": true,
"license": "MIT",
"dependencies": {
"postcss-value-parser": "^4.0.0",
"read-cache": "^1.0.0",
"resolve": "^1.1.7"
},
"engines": {
"node": ">=14.0.0"
},
"peerDependencies": {
"postcss": "^8.0.0"
}
},
"node_modules/postcss-js": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.1.0.tgz",
"integrity": "sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==",
"dev": true,
"funding": [
{
"type": "opencollective",
"url": "https://opencollective.com/postcss/"
},
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"license": "MIT",
"dependencies": {
"camelcase-css": "^2.0.1"
},
"engines": {
"node": "^12 || ^14 || >= 16"
},
"peerDependencies": {
"postcss": "^8.4.21"
}
},
"node_modules/postcss-load-config": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-6.0.1.tgz",
"integrity": "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==",
"dev": true,
"funding": [
{
"type": "opencollective",
"url": "https://opencollective.com/postcss/"
},
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"license": "MIT",
"dependencies": {
"lilconfig": "^3.1.1"
},
"engines": {
"node": ">= 18"
},
"peerDependencies": {
"jiti": ">=1.21.0",
"postcss": ">=8.0.9",
"tsx": "^4.8.1",
"yaml": "^2.4.2"
},
"peerDependenciesMeta": {
"jiti": {
"optional": true
},
"postcss": {
"optional": true
},
"tsx": {
"optional": true
},
"yaml": {
"optional": true
}
}
},
"node_modules/postcss-nested": {
"version": "6.2.0",
"resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz",
"integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==",
"dev": true,
"funding": [
{
"type": "opencollective",
"url": "https://opencollective.com/postcss/"
},
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"license": "MIT",
"dependencies": {
"postcss-selector-parser": "^6.1.1"
},
"engines": {
"node": ">=12.0"
},
"peerDependencies": {
"postcss": "^8.2.14"
}
},
"node_modules/postcss-selector-parser": {
"version": "6.1.2",
"resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz",
"integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==",
"dev": true,
"license": "MIT",
"dependencies": {
"cssesc": "^3.0.0",
"util-deprecate": "^1.0.2"
},
"engines": {
"node": ">=4"
}
},
"node_modules/postcss-value-parser": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz",
@ -2908,50 +2336,6 @@
"tweetnacl": "^1.0.3"
}
},
"node_modules/queue-microtask": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
"integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==",
"dev": true,
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "MIT"
},
"node_modules/read-cache": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
"integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==",
"dev": true,
"license": "MIT",
"dependencies": {
"pify": "^2.3.0"
}
},
"node_modules/readdirp": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
"integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
"dev": true,
"license": "MIT",
"dependencies": {
"picomatch": "^2.2.1"
},
"engines": {
"node": ">=8.10.0"
}
},
"node_modules/require-directory": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
@ -2962,39 +2346,6 @@
"node": ">=0.10.0"
}
},
"node_modules/resolve": {
"version": "1.22.12",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz",
"integrity": "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==",
"dev": true,
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
"is-core-module": "^2.16.1",
"path-parse": "^1.0.7",
"supports-preserve-symlinks-flag": "^1.0.0"
},
"bin": {
"resolve": "bin/resolve"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/reusify": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz",
"integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==",
"dev": true,
"license": "MIT",
"engines": {
"iojs": ">=1.0.0",
"node": ">=0.10.0"
}
},
"node_modules/rollup": {
"version": "4.60.4",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.4.tgz",
@ -3040,30 +2391,6 @@
"fsevents": "~2.3.2"
}
},
"node_modules/run-parallel": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
"integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==",
"dev": true,
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "MIT",
"dependencies": {
"queue-microtask": "^1.2.2"
}
},
"node_modules/rxjs": {
"version": "7.8.2",
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz",
@ -3155,29 +2482,6 @@
"node": ">=8"
}
},
"node_modules/sucrase": {
"version": "3.35.1",
"resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz",
"integrity": "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jridgewell/gen-mapping": "^0.3.2",
"commander": "^4.0.0",
"lines-and-columns": "^1.1.6",
"mz": "^2.7.0",
"pirates": "^4.0.1",
"tinyglobby": "^0.2.11",
"ts-interface-checker": "^0.1.9"
},
"bin": {
"sucrase": "bin/sucrase",
"sucrase-node": "bin/sucrase-node"
},
"engines": {
"node": ">=16 || 14 >=14.17"
}
},
"node_modules/supports-color": {
"version": "8.1.1",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz",
@ -3194,66 +2498,12 @@
"url": "https://github.com/chalk/supports-color?sponsor=1"
}
},
"node_modules/supports-preserve-symlinks-flag": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
"integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/tailwindcss": {
"version": "3.4.19",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.19.tgz",
"integrity": "sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==",
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.0.tgz",
"integrity": "sha512-y6nxMGB1nMW9R6k96e5gdIFzcfL/gTJRNaqGes1YvkLnPVXzWgbqFF2yLC0T8G774n24cx3Pe8XrKoniCOAH+Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"@alloc/quick-lru": "^5.2.0",
"arg": "^5.0.2",
"chokidar": "^3.6.0",
"didyoumean": "^1.2.2",
"dlv": "^1.1.3",
"fast-glob": "^3.3.2",
"glob-parent": "^6.0.2",
"is-glob": "^4.0.3",
"jiti": "^1.21.7",
"lilconfig": "^3.1.3",
"micromatch": "^4.0.8",
"normalize-path": "^3.0.0",
"object-hash": "^3.0.0",
"picocolors": "^1.1.1",
"postcss": "^8.4.47",
"postcss-import": "^15.1.0",
"postcss-js": "^4.0.1",
"postcss-load-config": "^4.0.2 || ^5.0 || ^6.0",
"postcss-nested": "^6.2.0",
"postcss-selector-parser": "^6.1.2",
"resolve": "^1.22.8",
"sucrase": "^3.35.0"
},
"bin": {
"tailwind": "lib/cli.js",
"tailwindcss": "lib/cli.js"
},
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/tailwindcss/node_modules/jiti": {
"version": "1.21.7",
"resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz",
"integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==",
"dev": true,
"license": "MIT",
"bin": {
"jiti": "bin/jiti.js"
}
"license": "MIT"
},
"node_modules/tapable": {
"version": "2.3.3",
@ -3269,29 +2519,6 @@
"url": "https://opencollective.com/webpack"
}
},
"node_modules/thenify": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz",
"integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==",
"dev": true,
"license": "MIT",
"dependencies": {
"any-promise": "^1.0.0"
}
},
"node_modules/thenify-all": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz",
"integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==",
"dev": true,
"license": "MIT",
"dependencies": {
"thenify": ">= 3.1.0 < 4"
},
"engines": {
"node": ">=0.8"
}
},
"node_modules/tinyglobby": {
"version": "0.2.16",
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz",
@ -3340,19 +2567,6 @@
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/to-regex-range": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
"integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"is-number": "^7.0.0"
},
"engines": {
"node": ">=8.0"
}
},
"node_modules/tree-kill": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz",
@ -3363,13 +2577,6 @@
"tree-kill": "cli.js"
}
},
"node_modules/ts-interface-checker": {
"version": "0.1.13",
"resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz",
"integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==",
"dev": true,
"license": "Apache-2.0"
},
"node_modules/tslib": {
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
@ -3414,13 +2621,6 @@
"browserslist": ">= 4.21.0"
}
},
"node_modules/util-deprecate": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
"dev": true,
"license": "MIT"
},
"node_modules/vite": {
"version": "7.3.3",
"resolved": "https://registry.npmjs.org/vite/-/vite-7.3.3.tgz",

View File

@ -14,7 +14,7 @@
"concurrently": "^9.0.1",
"laravel-vite-plugin": "^2.0.0",
"postcss": "^8.4.31",
"tailwindcss": "^3.4.19",
"tailwindcss": "^4.3.0",
"vite": "^7.0.7"
},
"dependencies": {

36
app/phpunit.xml Normal file
View File

@ -0,0 +1,36 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd"
bootstrap="vendor/autoload.php"
colors="true"
>
<testsuites>
<testsuite name="Unit">
<directory>tests/Unit</directory>
</testsuite>
<testsuite name="Feature">
<directory>tests/Feature</directory>
</testsuite>
</testsuites>
<source>
<include>
<directory>app</directory>
</include>
</source>
<php>
<env name="APP_ENV" value="testing"/>
<env name="APP_MAINTENANCE_DRIVER" value="file"/>
<env name="BCRYPT_ROUNDS" value="4"/>
<env name="BROADCAST_CONNECTION" value="null"/>
<env name="CACHE_STORE" value="array"/>
<env name="DB_CONNECTION" value="sqlite"/>
<env name="DB_DATABASE" value=":memory:"/>
<env name="DB_URL" value=""/>
<env name="MAIL_MAILER" value="array"/>
<env name="QUEUE_CONNECTION" value="sync"/>
<env name="SESSION_DRIVER" value="array"/>
<env name="PULSE_ENABLED" value="false"/>
<env name="TELESCOPE_ENABLED" value="false"/>
<env name="NIGHTWATCH_ENABLED" value="false"/>
</php>
</phpunit>

View File

@ -1,25 +1,331 @@
@import "tailwindcss";
@theme {
--color-bg: #0a0a0f;
--color-s0: #0c0c12;
--color-s1: #111117;
--color-s2: #16161e;
--color-s3: #1c1c26;
--color-hover: #1a1a24;
--color-surface: #101019;
--color-surface2: #1d1d28;
--color-b1: rgba(255,255,255,0.06);
--color-b2: rgba(255,255,255,0.10);
--color-t1: #f0f0f5;
--color-t2: #8888a0;
--color-t3: #44445a;
--color-dim: #5c5c70;
--color-akzent: #4f8ef7;
--color-akzent2: #7c5cff;
--color-success: #3ecf8e;
--color-warning: #f5a623;
--color-danger: #f05252;
--color-purple: #c084fc;
--font-sans: "Inter", system-ui, sans-serif;
--font-mono: "JetBrains Mono", monospace;
}
/* ── Keyframes ── */
@keyframes clu-pulse { 0%{transform:scale(.6);opacity:.5} 100%{transform:scale(2.4);opacity:0} }
@keyframes clu-type-in { to{opacity:1;transform:translateY(0)} }
@keyframes clu-blink { 50%{opacity:0} }
@keyframes clu-spin { to{transform:rotate(360deg)} }
@keyframes clu-dot-pulse { 0%,100%{opacity:1} 50%{opacity:.3} }
@layer utilities {
.dot-pulse { animation: clupilot-pulse 2s infinite; }
@keyframes clupilot-pulse { 0%,100%{opacity:1} 50%{opacity:.3} }
.dot-pulse { animation: clu-dot-pulse 2s infinite; }
.clu-reset { all: revert; }
.clu-spin { animation: clu-spin .8s linear infinite; }
}
@layer components {
/* Login ambient background */
.clu-login-body::before {
content:"";position:fixed;inset:0;z-index:0;pointer-events:none;
background:
radial-gradient(60% 50% at 12% 10%,rgba(79,142,247,.10),transparent 65%),
radial-gradient(50% 50% at 88% 100%,rgba(124,92,255,.08),transparent 65%);
}
.clu-login-body::after {
content:"";position:fixed;inset:0;z-index:0;pointer-events:none;
background-image:
linear-gradient(rgba(255,255,255,.018) 1px,transparent 1px),
linear-gradient(90deg,rgba(255,255,255,.018) 1px,transparent 1px);
background-size:32px 32px;
mask-image:radial-gradient(ellipse at center,#000 30%,transparent 75%);
}
/* Brand marks */
.clu-brand-mark {
background:linear-gradient(135deg,#4f8ef7 0%,#7c5cff 100%);
box-shadow:0 0 0 1px rgba(255,255,255,.06) inset,0 6px 16px rgba(79,142,247,.30);
display:grid;place-items:center;
}
.clu-brand-mark::before {
content:"";width:11px;height:11px;border-radius:50%;
background:#0a0a0f;box-shadow:inset 0 0 0 2px #fff;
}
.clu-brand-mark-sm {
background:linear-gradient(135deg,#4f8ef7 0%,#7c5cff 100%);
box-shadow:0 0 0 1px rgba(255,255,255,.06) inset,0 4px 12px rgba(79,142,247,.25);
display:grid;place-items:center;
}
.clu-brand-mark-sm::before {
content:"";width:10px;height:10px;border-radius:50%;
background:#0c0c12;box-shadow:inset 0 0 0 2px #fff;
}
/* Pulse dot */
.clu-pulse-dot { position:relative;flex:none; }
.clu-pulse-dot::after {
content:"";position:absolute;inset:-3px;border-radius:50%;
background:currentColor;opacity:.35;animation:clu-pulse 1.8s ease-out infinite;
}
/* Input with :focus-within */
.clu-input {
display:flex;align-items:center;gap:10px;padding:0 12px;
background:#101019;border:1px solid rgba(255,255,255,.06);border-radius:8px;
transition:border-color .15s,background .15s,box-shadow .15s;height:40px;
}
.clu-input:hover { border-color:rgba(255,255,255,.10); }
.clu-input:focus-within {
border-color:rgba(79,142,247,.50);background:#12121c;
box-shadow:0 0 0 3px rgba(79,142,247,.10);
}
.clu-input input {
flex:1;background:none;border:0;outline:0;
color:#f0f0f5;font-size:13.5px;font-family:inherit;min-width:0;height:100%;
}
.clu-input input::placeholder { color:#5c5c70; }
/* Divider */
.clu-divider {
display:flex;align-items:center;gap:12px;color:#5c5c70;
font-size:11px;font-family:var(--font-mono);text-transform:uppercase;letter-spacing:.08em;
}
.clu-divider::before,.clu-divider::after {
content:"";flex:1;height:1px;background:rgba(255,255,255,.06);
}
/* SSO button */
.clu-sso-btn {
display:flex;align-items:center;justify-content:center;gap:8px;padding:9px 12px;
background:#101019;border:1px solid rgba(255,255,255,.06);border-radius:8px;
color:#f0f0f5;font-size:12.5px;font-weight:500;transition:all .15s;cursor:pointer;font-family:inherit;
}
.clu-sso-btn:hover { background:#1d1d28;border-color:rgba(255,255,255,.10); }
/* Primary button */
.clu-btn-primary {
display:inline-flex;align-items:center;justify-content:center;gap:8px;
padding:10px 16px;background:#4f8ef7;color:#fff;border-radius:8px;
font-weight:500;font-size:13.5px;height:40px;width:100%;
transition:background .15s,transform .05s;border:none;cursor:pointer;font-family:inherit;
box-shadow:0 1px 0 rgba(255,255,255,.08) inset,0 1px 3px rgba(79,142,247,.35);
}
.clu-btn-primary:hover { background:#5e98f7; }
.clu-btn-primary:active { transform:translateY(1px); }
.clu-btn-primary:disabled { opacity:.7;cursor:not-allowed; }
.clu-btn-primary svg.arrow { transition:transform .15s; }
.clu-btn-primary:hover:not(:disabled) svg.arrow { transform:translateX(2px); }
/* Alt button */
.clu-alt-btn {
display:flex;align-items:center;gap:10px;padding:9px 12px;
background:transparent;border:1px solid rgba(255,255,255,.06);border-radius:8px;
font-size:12.5px;color:#f0f0f5;font-weight:500;transition:all .15s;
width:100%;cursor:pointer;font-family:inherit;
}
.clu-alt-btn:hover { border-color:rgba(255,255,255,.10);background:#101019; }
/* Live side glow */
.clu-live-side::before {
content:"";position:absolute;inset:0;pointer-events:none;
background:radial-gradient(80% 50% at 100% 0%,rgba(79,142,247,.12),transparent 65%);
}
/* Terminal */
.clu-term-line {
display:flex;gap:10px;align-items:flex-start;
opacity:0;transform:translateY(4px);animation:clu-type-in .35s ease forwards;
}
.clu-term-ok { color:#3ecf8e; }
.clu-term-warn { color:#f5a623; }
.clu-term-err { color:#f05252; }
.clu-term-acc { color:#4f8ef7; }
.clu-term-dim { color:#5c5c70; }
.clu-term-pkg { color:#c084fc; }
/* Cursor */
.clu-cursor {
display:inline-block;width:7px;height:13px;background:#4f8ef7;
vertical-align:-2px;animation:clu-blink 1s steps(1) infinite;margin-left:2px;
}
/* Dashboard topbar */
.clu-topbar {
backdrop-filter:blur(12px);-webkit-backdrop-filter:blur(12px);
background:rgba(10,10,15,.85);
}
/* Search */
.clu-search {
display:flex;align-items:center;gap:8px;padding:6px 10px;
background:#101019;border:1px solid rgba(255,255,255,.06);border-radius:7px;
color:#8888a0;transition:border-color .15s;min-width:280px;
}
.clu-search:hover { border-color:rgba(255,255,255,.10); }
.clu-search:focus-within { border-color:rgba(79,142,247,.5);background:#12121c; }
.clu-search input {
flex:1;background:none;border:0;outline:0;
color:#f0f0f5;font-size:12.5px;font-family:inherit;min-width:0;
}
.clu-search input::placeholder { color:#8888a0; }
/* Dashboard button */
.clu-btn-dash {
display:inline-flex;align-items:center;gap:6px;padding:7px 12px;
background:#4f8ef7;color:#fff;border-radius:7px;font-weight:500;font-size:12.5px;
transition:background .15s;border:none;cursor:pointer;font-family:inherit;
box-shadow:0 1px 0 rgba(255,255,255,.08) inset,0 1px 3px rgba(79,142,247,.35);
}
.clu-btn-dash:hover { background:#5e98f7; }
/* Metric top stripe */
.clu-metric { position:relative;overflow:hidden; }
.clu-metric::before { content:"";position:absolute;top:0;left:0;right:0;height:2px;background:#4f8ef7; }
.clu-metric-success::before { background:#3ecf8e; }
.clu-metric-warning::before { background:#f5a623; }
.clu-metric-danger::before { background:#f05252; }
.clu-metric-neutral::before { background:#5c5c70; }
/* Nav active bar */
.clu-nav-active { position:relative; }
.clu-nav-active::before {
content:"";position:absolute;left:-12px;top:6px;bottom:6px;
width:2px;border-radius:2px;background:#4f8ef7;
}
/* Row actions */
.clu-row-actions { display:flex;gap:4px;justify-content:flex-end;opacity:0;transition:opacity .15s; }
tr:hover .clu-row-actions { opacity:1; }
/* Plan box overlay */
.clu-plan-box { position:relative;overflow:hidden; }
.clu-plan-box::before {
content:"";position:absolute;inset:0;pointer-events:none;
background:radial-gradient(120% 80% at 0% 0%,rgba(79,142,247,.10),transparent 60%);
}
/* Chart axis */
.clu-chart-axis { font-family:var(--font-mono);font-size:9.5px;fill:#5c5c70; }
/* ── Stepper ── */
.clu-stepper { display:flex;align-items:center;gap:10px;margin-top:28px;font-family:var(--font-mono);font-size:11px;color:#44445a; }
.clu-step { display:flex;align-items:center;gap:7px; }
.clu-step-num { width:20px;height:20px;border-radius:50%;border:1px solid rgba(255,255,255,.06);display:grid;place-items:center;font-size:10.5px;color:#8888a0;background:#101019; }
.clu-step.active .clu-step-num { border-color:#4f8ef7;color:#4f8ef7;background:rgba(79,142,247,.12); }
.clu-step.done .clu-step-num { border-color:#3ecf8e;color:#0a0a0f;background:#3ecf8e; }
.clu-step-label { font-size:11px;color:#8888a0;text-transform:uppercase;letter-spacing:.06em;font-weight:500; }
.clu-step.active .clu-step-label { color:#f0f0f5; }
.clu-step-line { flex:1;height:1px;background:rgba(255,255,255,.06);min-width:14px;max-width:28px; }
/* ── Two-column field row ── */
.clu-field-row { display:grid;grid-template-columns:1fr 1fr;gap:10px; }
/* ── Input suffix ── */
.clu-input.with-suffix { padding-right:0; }
.clu-input-suffix { height:100%;display:flex;align-items:center;padding:0 12px;border-left:1px solid rgba(255,255,255,.06);color:#8888a0;font-family:var(--font-mono);font-size:11.5px;background:rgba(255,255,255,.015);border-top-right-radius:8px;border-bottom-right-radius:8px; }
/* ── Input valid checkmark ── */
.clu-input-check { color:#3ecf8e;display:none;flex:none; }
.clu-input.valid .clu-input-check { display:inline-flex; }
/* ── Slug preview ── */
.clu-slug-preview { font-family:var(--font-mono);font-size:11px;color:#8888a0;margin-top:4px; }
.clu-slug-preview b { color:#4f8ef7;font-weight:500; }
/* ── Password strength meter ── */
.clu-pw-meter { display:flex;gap:4px;margin-top:6px; }
.clu-pw-meter span { flex:1;height:3px;border-radius:2px;background:#16161e;transition:background .2s; }
.clu-pw-meter.s1 span:nth-child(-n+1) { background:#f05252; }
.clu-pw-meter.s2 span:nth-child(-n+2) { background:#f5a623; }
.clu-pw-meter.s3 span:nth-child(-n+3) { background:#4f8ef7; }
.clu-pw-meter.s4 span { background:#3ecf8e; }
.clu-pw-rules { display:flex;flex-wrap:wrap;gap:6px 12px;margin-top:6px;font-family:var(--font-mono);font-size:10.5px;color:#5c5c70; }
.clu-pw-rule { display:inline-flex;align-items:center;gap:5px; }
.clu-pw-rule .clu-pw-bullet { width:5px;height:5px;border-radius:50%;background:#5c5c70;transition:background .15s; }
.clu-pw-rule.ok { color:#3ecf8e; }
.clu-pw-rule.ok .clu-pw-bullet { background:#3ecf8e; }
/* ── Checkbox ── */
.clu-checkbox { display:flex;align-items:flex-start;gap:10px;font-size:12.5px;color:#8888a0;cursor:pointer;user-select:none;line-height:1.45; }
.clu-checkbox .clu-check-box { width:15px;height:15px;border-radius:4px;border:1px solid rgba(255,255,255,.10);background:#101019;display:grid;place-items:center;transition:all .15s;flex:none;margin-top:1px; }
.clu-checkbox input:checked ~ .clu-check-box { background:#4f8ef7;border-color:#4f8ef7; }
.clu-checkbox input:checked ~ .clu-check-box svg { opacity:1;transform:scale(1); }
.clu-checkbox .clu-check-box svg { opacity:0;transform:scale(.6);transition:all .15s;color:#fff; }
.clu-checkbox:hover { color:#f0f0f5; }
/* ── Trial card (signup right panel) ── */
.clu-trial-card { background:#16161e;border:1px solid rgba(255,255,255,.06);border-radius:12px;padding:18px 20px;position:relative;overflow:hidden; }
.clu-trial-card::before { content:"";position:absolute;top:0;left:0;right:0;height:2px;background:linear-gradient(90deg,#4f8ef7,#7c5cff); }
.clu-trial-row { display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:8px; }
.clu-trial-tag { font-family:var(--font-mono);font-size:10.5px;color:#4f8ef7;background:rgba(79,142,247,.12);padding:3px 8px;border-radius:5px;border:1px solid rgba(79,142,247,.25);font-weight:500;text-transform:uppercase;letter-spacing:.05em; }
.clu-trial-count { font-family:var(--font-mono);font-size:11px;color:#8888a0;display:inline-flex;align-items:center;gap:6px; }
.clu-trial-title { font-size:22px;font-weight:600;letter-spacing:-.015em;line-height:1.25;margin-top:12px; }
.clu-trial-title em { font-style:normal;background:linear-gradient(90deg,#4f8ef7,#a78bfa);-webkit-background-clip:text;background-clip:text;color:transparent;font-weight:700; }
.clu-trial-sub { color:#8888a0;font-size:13px;margin-top:6px; }
/* ── Benefit list ── */
.clu-benefit-list { list-style:none;padding:0;margin:0;display:flex;flex-direction:column;gap:14px; }
.clu-benefit { display:flex;gap:12px;align-items:flex-start; }
.clu-benefit-ico { width:30px;height:30px;border-radius:8px;background:rgba(79,142,247,.12);border:1px solid rgba(79,142,247,.22);color:#4f8ef7;display:grid;place-items:center;flex:none; }
.clu-benefit-ico.success { background:rgba(62,207,142,.12);border-color:rgba(62,207,142,.22);color:#3ecf8e; }
.clu-benefit-ico.purple { background:rgba(124,92,255,.12);border-color:rgba(124,92,255,.25);color:#a78bfa; }
.clu-benefit-ico.warn { background:rgba(245,166,35,.10);border-color:rgba(245,166,35,.22);color:#f5a623; }
.clu-benefit-title { font-size:13.5px;font-weight:500;color:#f0f0f5; }
.clu-benefit-desc { font-size:12.5px;color:#8888a0;margin-top:2px;line-height:1.5; }
/* ── Quote card ── */
.clu-quote-card { background:#16161e;border:1px solid rgba(255,255,255,.06);border-radius:12px;padding:18px 20px;margin-top:auto; }
.clu-quote-text { font-size:13.5px;color:#dcdce8;line-height:1.55; }
.clu-quote-text::before { content:"\201E";color:#4f8ef7;font-size:24px;line-height:0;margin-right:4px;vertical-align:-6px;font-weight:600; }
.clu-quote-text::after { content:"\201D";color:#4f8ef7;font-weight:600; }
.clu-quote-author { display:flex;align-items:center;gap:10px;margin-top:12px; }
.clu-avatar { width:32px;height:32px;border-radius:50%;background:linear-gradient(135deg,#3ecf8e,#2a9d6f);display:grid;place-items:center;color:#0a0a0f;font-weight:600;font-size:12px;font-family:var(--font-mono); }
.clu-author-name { font-size:12px;color:#f0f0f5;font-weight:500; }
.clu-author-role { font-size:11px;color:#8888a0;font-family:var(--font-mono); }
/* ── Logo strip ── */
.clu-logos { display:flex;align-items:center;gap:20px;flex-wrap:wrap;opacity:.55;font-family:var(--font-mono);font-size:11px;color:#8888a0;text-transform:uppercase;letter-spacing:.08em; }
.clu-logo-pill { padding:5px 10px;border:1px solid rgba(255,255,255,.06);border-radius:5px;background:#101019; }
/* Login page responsive grid */
.clu-login-grid {
display:grid;
grid-template-columns:minmax(420px,1fr) 1fr;
min-height:100vh;
min-height:100dvh;
position:relative;
z-index:10;
}
.clu-form-side { padding:32px 48px; }
.clu-sso-row { display:grid;grid-template-columns:1fr 1fr;gap:8px; }
@media(max-width:1080px){
.clu-login-grid { grid-template-columns:1fr; }
.clu-live-side { display:none !important; }
.clu-form-side { padding:28px 36px; }
}
@media(max-width:640px){
.clu-form-side { padding:20px; }
.clu-sso-row { grid-template-columns:1fr; }
.clu-login-status { display:none !important; }
.clu-field-row { grid-template-columns:1fr; }
.clu-step-label { display:none; }
}
@media(max-width:390px){
.clu-form-side { padding:16px; }
}
}

View File

@ -1 +1,4 @@
import './bootstrap';
import './login-effects';
import './register-effects';
window.__clupilot = 1;

View File

@ -0,0 +1,47 @@
function initLoginEffects() {
const eyeBtn = document.getElementById('toggle-eye');
const pwd = document.getElementById('password');
if (eyeBtn && pwd && !eyeBtn.dataset.bound) {
eyeBtn.dataset.bound = '1';
eyeBtn.addEventListener('click', () => {
pwd.type = pwd.type === 'password' ? 'text' : 'password';
});
}
const body = document.getElementById('term-body');
if (!body || body.dataset.bound) return;
body.dataset.bound = '1';
const lines = [
['09:42:18', '<span class="clu-term-acc font-semibold">$</span> <span class="clu-term-dim">clupilot</span> status --cluster=acme'],
['09:42:18', '<span class="clu-term-ok">✓</span> 87/87 sites <span class="clu-term-dim">healthy</span> · <span class="clu-term-acc">142ms</span> ø response'],
['09:42:19', '<span class="clu-term-ok">✓</span> backup eu-prod-02 → <span class="clu-term-pkg">s3://clupilot-backups</span> <span class="clu-term-dim">248 MB</span>'],
['09:42:21', '<span class="clu-term-acc">→</span> deploying <span class="clu-term-pkg">Yoast-SEO@22.5</span> to <span class="clu-term-dim">3 sites…</span>'],
['09:42:23', '<span class="clu-term-ok">✓</span> agentur-helio.com <span class="clu-term-dim">(312ms)</span>'],
['09:42:24', '<span class="clu-term-ok">✓</span> blog.fintech-radar.io <span class="clu-term-dim">(287ms)</span>'],
['09:42:24', '<span class="clu-term-ok">✓</span> docs.opencrate.dev <span class="clu-term-dim">(298ms)</span>'],
['09:42:26', '<span class="clu-term-warn">!</span> classic.tonhoff-bau.de <span class="clu-term-dim">PHP 7.4 EOL — migration queued</span>'],
['09:42:28', '<span class="clu-term-err">✗</span> fw <span class="clu-term-dim">blocked</span> 17 attempts from <span class="clu-term-acc">185.220.101.18</span> <span class="clu-term-dim">(TOR)</span>'],
['09:42:30', '<span class="clu-term-ok">✓</span> SSL renewed: <span class="clu-term-pkg">*.kunde-mueller.de</span> <span class="clu-term-dim">until 2026-08-25</span>'],
['09:42:32', '<span class="clu-term-acc font-semibold">$</span> <span class="clu-term-dim">tail -f /var/log/clupilot/audit.log</span><span class="clu-cursor"></span>'],
];
let i = 0;
function addLine() {
if (i >= lines.length) {
setTimeout(() => { body.innerHTML = ''; i = 0; addLine(); }, 5000);
return;
}
const [t, html] = lines[i++];
const el = document.createElement('div');
el.className = 'clu-term-line';
el.innerHTML =
'<div class="clu-term-dim flex-none text-[10.5px] pt-[1px]" style="width:60px;">' + t + '</div>' +
'<div style="color:#cccddd;flex:1;min-width:0;word-break:break-word;">' + html + '</div>';
body.appendChild(el);
body.scrollTop = body.scrollHeight;
while (body.children.length > 11) body.removeChild(body.firstChild);
setTimeout(addLine, 600 + Math.random() * 450);
}
addLine();
}
document.addEventListener('DOMContentLoaded', initLoginEffects);
document.addEventListener('livewire:navigated', initLoginEffects);

View File

@ -0,0 +1,59 @@
function initRegisterEffects() {
// Workspace slug preview
const ws = document.getElementById('reg-workspace');
const slugEl = document.getElementById('reg-slug-preview');
if (ws && slugEl && !ws.dataset.bound) {
ws.dataset.bound = '1';
const slugify = s => s.toLowerCase().trim()
.replace(/[^a-z0-9]+/g, '-')
.replace(/^-|-$/g, '')
.slice(0, 32) || 'dein-team';
ws.addEventListener('input', () => { slugEl.textContent = slugify(ws.value); });
}
// Email valid checkmark
const emailInput = document.getElementById('reg-email');
const emailWrap = document.getElementById('reg-email-wrap');
if (emailInput && emailWrap && !emailInput.dataset.bound) {
emailInput.dataset.bound = '1';
emailInput.addEventListener('input', () => {
emailWrap.classList.toggle('valid', /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(emailInput.value));
});
}
// Password strength meter + rules
const pwd = document.getElementById('reg-password');
const meter = document.getElementById('reg-pw-meter');
if (pwd && meter && !pwd.dataset.bound) {
pwd.dataset.bound = '1';
pwd.addEventListener('input', () => {
const v = pwd.value;
const tests = {
len: v.length >= 10,
upper: /[A-Z]/.test(v),
num: /[0-9]/.test(v),
sym: /[^A-Za-z0-9]/.test(v),
};
let score = 0;
Object.keys(tests).forEach(k => {
const ok = tests[k];
const el = document.querySelector('.clu-pw-rule[data-rule="' + k + '"]');
if (el) { el.classList.toggle('ok', ok); }
if (ok) score++;
});
meter.className = 'clu-pw-meter' + (v.length ? ' s' + score : '');
});
}
// Password eye toggle
const eyeBtn = document.getElementById('reg-toggle-eye');
if (pwd && eyeBtn && !eyeBtn.dataset.bound) {
eyeBtn.dataset.bound = '1';
eyeBtn.addEventListener('click', () => {
pwd.type = pwd.type === 'password' ? 'text' : 'password';
});
}
}
document.addEventListener('DOMContentLoaded', initRegisterEffects);
document.addEventListener('livewire:navigated', initRegisterEffects);

View File

@ -1,17 +1,704 @@
<x-app-layout>
<x-slot name="header">
<h2 class="font-semibold text-xl text-gray-800 leading-tight">
{{ __('Dashboard') }}
</h2>
</x-slot>
@extends('layouts.dash')
<div class="py-12">
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
<div class="bg-white overflow-hidden shadow-sm sm:rounded-lg">
<div class="p-6 text-gray-900">
{{ __("You're logged in!") }}
@section('content')
<div class="flex min-h-screen" style="display:grid;grid-template-columns:220px 1fr;">
{{-- ===== SIDEBAR ===== --}}
<aside class="sticky top-0 h-screen flex flex-col border-r border-white/6 bg-s0">
{{-- HEADER (fixed) --}}
<div class="flex-none" style="padding:16px 12px 0;">
{{-- Brand --}}
<div class="flex items-center gap-[9px] border-b border-white/6" style="padding:4px 6px 12px;">
<div class="clu-brand-mark-sm w-[26px] h-[26px] rounded-[7px] flex-none" aria-hidden="true"></div>
<div class="font-semibold text-[14px] tracking-[-0.01em]">
CluPilot<span class="text-t2 font-medium ml-1 text-[11px]">v2.4</span>
</div>
</div>
{{-- Workspace switch --}}
<button class="flex items-center gap-[9px] border border-white/6 rounded-md bg-surface transition-colors hover:bg-[#14141e] cursor-pointer w-full" style="padding:8px 9px;margin-top:12px;" aria-label="Workspace wechseln">
<div class="w-[22px] h-[22px] rounded-[5px] grid place-items-center text-bg font-bold text-[11px] flex-none" style="background:linear-gradient(135deg,#3ecf8e,#2a9d6f);">AC</div>
<div class="flex-1 min-w-0 text-left">
<div class="text-[12.5px] font-medium truncate">Acme Cluster</div>
<div class="text-[10.5px] text-t2 font-mono">team · 14 user</div>
</div>
<svg class="text-dim flex-none" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true">
<path d="M8 9l4-4 4 4M8 15l4 4 4-4"/>
</svg>
</button>
</div>
{{-- Navigation (scrollable) --}}
<nav class="flex flex-col gap-[14px] flex-1 overflow-y-auto min-h-0" style="padding:12px 12px 0;">
<div class="flex flex-col gap-[1px]">
<div class="text-[10.5px] uppercase tracking-[.08em] text-dim font-semibold" style="padding:6px 9px 4px;">Übersicht</div>
<div class="clu-nav-active flex items-center gap-[9px] rounded-md text-t1 cursor-pointer text-[12.5px] transition-all bg-[#181822]" style="padding:6.5px 9px;">
<svg class="w-[15px] h-[15px] text-akzent flex-none" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" aria-hidden="true"><rect x="3" y="3" width="7" height="9" rx="1.5"/><rect x="14" y="3" width="7" height="5" rx="1.5"/><rect x="14" y="12" width="7" height="9" rx="1.5"/><rect x="3" y="16" width="7" height="5" rx="1.5"/></svg>
<span>Dashboard</span>
</div>
<div class="flex items-center gap-[9px] rounded-md text-[#c8c8d8] cursor-pointer text-[12.5px] transition-all hover:bg-[#14141e] hover:text-t1" style="padding:6.5px 9px;">
<svg class="w-[15px] h-[15px] text-t2 flex-none" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" aria-hidden="true"><path d="M3 12h4l3-7 4 14 3-7h4"/></svg>
<span>Metriken</span>
<span class="ml-auto text-[10.5px] font-mono text-t2 bg-[#14141e] px-[6px] py-[1px] rounded-[10px] border border-white/6">live</span>
</div>
<div class="flex items-center gap-[9px] rounded-md text-[#c8c8d8] cursor-pointer text-[12.5px] transition-all hover:bg-[#14141e] hover:text-t1" style="padding:6.5px 9px;">
<svg class="w-[15px] h-[15px] text-t2 flex-none" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" aria-hidden="true"><circle cx="12" cy="12" r="9"/><path d="M12 7v5l3 2"/></svg>
<span>Aktivität</span>
</div>
</div>
</x-app-layout>
<div class="flex flex-col gap-[1px]">
<div class="text-[10.5px] uppercase tracking-[.08em] text-dim font-semibold" style="padding:6px 9px 4px;">Infrastruktur</div>
<div class="flex items-center gap-[9px] rounded-md text-[#c8c8d8] cursor-pointer text-[12.5px] transition-all hover:bg-[#14141e] hover:text-t1" style="padding:6.5px 9px;">
<svg class="w-[15px] h-[15px] text-t2 flex-none" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" aria-hidden="true"><rect x="3" y="4" width="18" height="7" rx="1.5"/><rect x="3" y="13" width="18" height="7" rx="1.5"/><circle cx="7" cy="7.5" r="0.7" fill="currentColor"/><circle cx="7" cy="16.5" r="0.7" fill="currentColor"/></svg>
<span>Server</span>
<span class="ml-auto text-[10.5px] font-mono text-t2 bg-[#14141e] px-[6px] py-[1px] rounded-[10px] border border-white/6">6</span>
</div>
<div class="flex items-center gap-[9px] rounded-md text-[#c8c8d8] cursor-pointer text-[12.5px] transition-all hover:bg-[#14141e] hover:text-t1" style="padding:6.5px 9px;">
<svg class="w-[15px] h-[15px] text-t2 flex-none" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" aria-hidden="true"><path d="M5 12V7a3 3 0 0 1 3-3h8a3 3 0 0 1 3 3v5M3 12h18l-2 8H5l-2-8z"/></svg>
<span>Cluster</span>
</div>
<div class="flex items-center gap-[9px] rounded-md text-[#c8c8d8] cursor-pointer text-[12.5px] transition-all hover:bg-[#14141e] hover:text-t1" style="padding:6.5px 9px;">
<svg class="w-[15px] h-[15px] text-t2 flex-none" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" aria-hidden="true"><path d="M9 17l-5 3v-7l5 3zM15 17l5 3v-7l-5 3zM12 4l5 3v6l-5 3-5-3V7l5-3z"/></svg>
<span>SSH Keys</span>
</div>
<div class="flex items-center gap-[9px] rounded-md text-[#c8c8d8] cursor-pointer text-[12.5px] transition-all hover:bg-[#14141e] hover:text-t1" style="padding:6.5px 9px;">
<svg class="w-[15px] h-[15px] text-t2 flex-none" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" aria-hidden="true"><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"/></svg>
<span>DNS &amp; Domains</span>
</div>
</div>
<div class="flex flex-col gap-[1px]">
<div class="text-[10.5px] uppercase tracking-[.08em] text-dim font-semibold" style="padding:6px 9px 4px;">WordPress</div>
<div class="flex items-center gap-[9px] rounded-md text-[#c8c8d8] cursor-pointer text-[12.5px] transition-all hover:bg-[#14141e] hover:text-t1" style="padding:6.5px 9px;">
<svg class="w-[15px] h-[15px] text-t2 flex-none" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" aria-hidden="true"><circle cx="12" cy="12" r="9"/><path d="M3 12h18M12 3v18"/></svg>
<span>Sites</span>
<span class="ml-auto text-[10.5px] font-mono text-t2 bg-[#14141e] px-[6px] py-[1px] rounded-[10px] border border-white/6">87</span>
</div>
<div class="flex items-center gap-[9px] rounded-md text-[#c8c8d8] cursor-pointer text-[12.5px] transition-all hover:bg-[#14141e] hover:text-t1" style="padding:6.5px 9px;">
<svg class="w-[15px] h-[15px] text-t2 flex-none" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" aria-hidden="true"><path d="M12 4v16M4 12h16"/><circle cx="12" cy="12" r="9"/></svg>
<span>Plugins</span>
</div>
<div class="flex items-center gap-[9px] rounded-md text-[#c8c8d8] cursor-pointer text-[12.5px] transition-all hover:bg-[#14141e] hover:text-t1" style="padding:6.5px 9px;">
<svg class="w-[15px] h-[15px] text-t2 flex-none" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" aria-hidden="true"><path d="M3 6h18M3 12h18M3 18h18"/></svg>
<span>Themes</span>
</div>
<div class="flex items-center gap-[9px] rounded-md text-[#c8c8d8] cursor-pointer text-[12.5px] transition-all hover:bg-[#14141e] hover:text-t1" style="padding:6.5px 9px;">
<svg class="w-[15px] h-[15px] text-t2 flex-none" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" aria-hidden="true"><path d="M21 12c0 5-9 9-9 9s-9-4-9-9V5l9-3 9 3v7z"/><path d="M9 11l2 2 4-4"/></svg>
<span>Backups</span>
</div>
</div>
<div class="flex flex-col gap-[1px]">
<div class="text-[10.5px] uppercase tracking-[.08em] text-dim font-semibold" style="padding:6px 9px 4px;">Sicherheit</div>
<div class="flex items-center gap-[9px] rounded-md text-[#c8c8d8] cursor-pointer text-[12.5px] transition-all hover:bg-[#14141e] hover:text-t1" style="padding:6.5px 9px;">
<svg class="w-[15px] h-[15px] text-t2 flex-none" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" aria-hidden="true"><path d="M12 3l8 4v5c0 5-3.5 8-8 9-4.5-1-8-4-8-9V7l8-4z"/></svg>
<span>Firewall</span>
<span class="ml-auto text-[10.5px] font-mono text-warning bg-warning/12 px-[6px] py-[1px] rounded-[10px] border border-warning/25">3</span>
</div>
<div class="flex items-center gap-[9px] rounded-md text-[#c8c8d8] cursor-pointer text-[12.5px] transition-all hover:bg-[#14141e] hover:text-t1" style="padding:6.5px 9px;">
<svg class="w-[15px] h-[15px] text-t2 flex-none" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" aria-hidden="true"><circle cx="12" cy="12" r="9"/><path d="M12 8v4l3 2"/></svg>
<span>Audit-Log</span>
</div>
<div class="flex items-center gap-[9px] rounded-md text-[#c8c8d8] cursor-pointer text-[12.5px] transition-all hover:bg-[#14141e] hover:text-t1" style="padding:6.5px 9px;">
<svg class="w-[15px] h-[15px] text-t2 flex-none" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" aria-hidden="true"><rect x="5" y="11" width="14" height="9" rx="1.5"/><path d="M8 11V7a4 4 0 0 1 8 0v4"/></svg>
<span>Zertifikate</span>
</div>
</div>
<div class="flex flex-col gap-[1px]">
<div class="text-[10.5px] uppercase tracking-[.08em] text-dim font-semibold" style="padding:6px 9px 4px;">Einstellungen</div>
<div class="flex items-center gap-[9px] rounded-md text-[#c8c8d8] cursor-pointer text-[12.5px] transition-all hover:bg-[#14141e] hover:text-t1" style="padding:6.5px 9px;">
<svg class="w-[15px] h-[15px] text-t2 flex-none" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" aria-hidden="true"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.7 1.7 0 0 0 .3 1.8l.1.1a2 2 0 1 1-2.8 2.8l-.1-.1a1.7 1.7 0 0 0-1.8-.3 1.7 1.7 0 0 0-1 1.5V21a2 2 0 1 1-4 0v-.1a1.7 1.7 0 0 0-1.1-1.5 1.7 1.7 0 0 0-1.8.3l-.1.1a2 2 0 1 1-2.8-2.8l.1-.1a1.7 1.7 0 0 0 .3-1.8 1.7 1.7 0 0 0-1.5-1H3a2 2 0 1 1 0-4h.1A1.7 1.7 0 0 0 4.6 9a1.7 1.7 0 0 0-.3-1.8l-.1-.1a2 2 0 1 1 2.8-2.8l.1.1a1.7 1.7 0 0 0 1.8.3H9a1.7 1.7 0 0 0 1-1.5V3a2 2 0 1 1 4 0v.1a1.7 1.7 0 0 0 1 1.5 1.7 1.7 0 0 0 1.8-.3l.1-.1a2 2 0 1 1 2.8 2.8l-.1.1a1.7 1.7 0 0 0-.3 1.8V9a1.7 1.7 0 0 0 1.5 1H21a2 2 0 1 1 0 4h-.1a1.7 1.7 0 0 0-1.5 1z"/></svg>
<span>Allgemein</span>
</div>
<div class="flex items-center gap-[9px] rounded-md text-[#c8c8d8] cursor-pointer text-[12.5px] transition-all hover:bg-[#14141e] hover:text-t1" style="padding:6.5px 9px;">
<svg class="w-[15px] h-[15px] text-t2 flex-none" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" aria-hidden="true"><path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M22 21v-2a4 4 0 0 0-3-3.9M16 3.1a4 4 0 0 1 0 7.8"/></svg>
<span>Team</span>
</div>
<div class="flex items-center gap-[9px] rounded-md text-[#c8c8d8] cursor-pointer text-[12.5px] transition-all hover:bg-[#14141e] hover:text-t1" style="padding:6.5px 9px;">
<svg class="w-[15px] h-[15px] text-t2 flex-none" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" aria-hidden="true"><rect x="3" y="6" width="18" height="13" rx="2"/><path d="M3 10h18"/></svg>
<span>Abrechnung</span>
</div>
</div>
</nav>
{{-- FOOTER (fixed) --}}
<div class="flex-none border-t border-white/6" style="padding:12px;">
{{-- Plan box --}}
<div class="clu-plan-box border border-white/6 rounded-[10px] p-3" style="background:linear-gradient(180deg,#11111a 0%,#0e0e15 100%);">
<div class="relative flex items-center justify-between text-[11.5px]">
<span class="font-semibold">Team Plan</span>
<span class="font-mono text-[10px] text-akzent bg-akzent/12 px-[6px] py-[2px] rounded-[4px] border border-akzent/25">PRO</span>
</div>
<div class="relative mt-[9px] h-[5px] rounded-[3px] bg-[#191923] overflow-hidden">
<span class="block h-full w-[62%] rounded-[3px]" style="background:linear-gradient(90deg,#4f8ef7,#7c5cff);"></span>
</div>
<div class="relative flex justify-between mt-[6px] text-[10.5px] text-t2 font-mono">
<span>87 / 140 sites</span><span>62%</span>
</div>
<button class="relative mt-[10px] w-full border border-white/10 rounded-md text-[11.5px] font-medium text-t1 bg-s2 transition-colors hover:bg-s3" style="padding:7px;">
Upgrade auf Scale
</button>
</div>
{{-- Logout --}}
<form method="POST" action="{{ route('logout') }}" class="mt-[8px]">
@csrf
<button type="submit" class="flex items-center gap-[9px] w-full rounded-md text-t2 text-[12.5px] font-medium transition-all hover:bg-[#14141e] hover:text-t1" style="padding:7px 9px;">
<svg class="flex-none" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" aria-hidden="true">
<path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4M16 17l5-5-5-5M21 12H9"/>
</svg>
Abmelden
</button>
</form>
</div>
</aside>
{{-- ===== MAIN ===== --}}
<main class="flex flex-col min-w-0">
{{-- Topbar --}}
<div class="clu-topbar sticky top-0 z-[5] flex items-center gap-[14px] border-b border-white/6" style="padding:14px 22px;">
<div class="flex items-center gap-2 min-w-0">
<span class="text-t2 text-[12.5px]">Acme Cluster</span>
<span class="text-dim text-[12.5px]">/</span>
<span class="text-[14.5px] font-semibold tracking-[-0.01em]">Dashboard</span>
</div>
<span class="inline-flex items-center gap-[6px] px-2 py-[3px] border border-success/25 bg-success/12 text-success rounded-full text-[11px] font-medium font-mono">
<span class="clu-pulse-dot w-[6px] h-[6px] rounded-full bg-current"></span>
All systems operational
</span>
<div class="flex-1"></div>
<div class="clu-search">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><circle cx="11" cy="11" r="7"/><path d="M21 21l-4.3-4.3"/></svg>
<input placeholder="Sites, Server, Domains suchen…" />
<kbd class="font-mono text-[10.5px] text-t2 px-[5px] py-[1px] border border-white/6 rounded-[4px] bg-s0">⌘K</kbd>
</div>
<button class="relative w-8 h-8 border border-white/6 rounded-[7px] grid place-items-center text-t2 bg-surface transition-all hover:text-t1 hover:border-white/10 hover:bg-[#15151f]" aria-label="Dokumentation">
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><circle cx="12" cy="12" r="9"/><path d="M9.1 9a3 3 0 0 1 5.8 1c0 2-3 3-3 3M12 17h.01"/></svg>
</button>
<button class="relative w-8 h-8 border border-white/6 rounded-[7px] grid place-items-center text-t2 bg-surface transition-all hover:text-t1 hover:border-white/10 hover:bg-[#15151f]" aria-label="Benachrichtigungen">
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path d="M6 8a6 6 0 0 1 12 0c0 7 3 9 3 9H3s3-2 3-9M10 21a2 2 0 0 0 4 0"/></svg>
<span class="absolute -top-[3px] -right-[3px] bg-danger text-white text-[9.5px] font-semibold min-w-[16px] h-4 rounded-full grid place-items-center px-1 border-2 border-bg font-mono">3</span>
</button>
<button class="clu-btn-dash">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" aria-hidden="true"><path d="M12 5v14M5 12h14"/></svg>
Server
</button>
</div>
{{-- Content --}}
<div class="flex flex-col gap-[22px] w-full mx-auto" style="padding:22px;max-width:1600px;">
{{-- METRICS --}}
<div style="display:grid;grid-template-columns:repeat(6,1fr);gap:12px;">
<div class="clu-metric clu-metric-success bg-s2 border border-white/6 rounded-[10px] transition-colors hover:border-white/10" style="padding:14px 14px 13px;">
<div class="flex items-center gap-[7px] text-t2 text-[11.5px] font-medium">
<svg class="w-[13px] h-[13px]" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><circle cx="12" cy="12" r="9"/><path d="M9 12l2 2 4-4"/></svg>
Sites Online
</div>
<div class="font-mono text-[22px] font-semibold tracking-[-0.02em] leading-[1.1] mt-2">
87<span class="text-[13px] text-t2 font-medium ml-[2px]">/87</span>
</div>
<div class="flex items-center gap-[6px] text-t2 text-[11px] mt-[6px]">
<span class="font-mono font-medium text-success">100%</span> Verfügbarkeit · 24h
</div>
<svg class="absolute right-[10px] bottom-[10px] opacity-70" width="60" height="22" viewBox="0 0 60 22" aria-hidden="true"><polyline fill="none" stroke="#3ecf8e" stroke-width="1.5" points="0,16 8,14 16,15 24,12 32,13 40,10 48,11 60,8"/></svg>
</div>
<div class="clu-metric bg-s2 border border-white/6 rounded-[10px] transition-colors hover:border-white/10" style="padding:14px 14px 13px;">
<div class="flex items-center gap-[7px] text-t2 text-[11.5px] font-medium">
<svg class="w-[13px] h-[13px]" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><circle cx="12" cy="12" r="9"/><path d="M12 7v5l3 2"/></svg>
Ø Response
</div>
<div class="font-mono text-[22px] font-semibold tracking-[-0.02em] leading-[1.1] mt-2">
142<span class="text-[13px] text-t2 font-medium ml-[2px]">ms</span>
</div>
<div class="flex items-center gap-[6px] text-t2 text-[11px] mt-[6px]">
<span class="font-mono font-medium text-success"> 8.4%</span> vs. letzte Woche
</div>
<svg class="absolute right-[10px] bottom-[10px] opacity-70" width="60" height="22" viewBox="0 0 60 22" aria-hidden="true"><polyline fill="none" stroke="#4f8ef7" stroke-width="1.5" points="0,8 8,12 16,9 24,14 32,11 40,10 48,7 60,9"/></svg>
</div>
<div class="clu-metric clu-metric-warning bg-s2 border border-white/6 rounded-[10px] transition-colors hover:border-white/10" style="padding:14px 14px 13px;">
<div class="flex items-center gap-[7px] text-t2 text-[11.5px] font-medium">
<svg class="w-[13px] h-[13px]" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path d="M21 12a9 9 0 1 1-3-6.7M21 4v5h-5"/></svg>
Updates Pending
</div>
<div class="font-mono text-[22px] font-semibold tracking-[-0.02em] leading-[1.1] mt-2">24</div>
<div class="flex items-center gap-[6px] text-t2 text-[11px] mt-[6px]">
<span class="font-mono font-medium text-warning">7 kritisch</span> · 17 minor
</div>
<svg class="absolute right-[10px] bottom-[10px] opacity-70" width="60" height="22" viewBox="0 0 60 22" aria-hidden="true"><polyline fill="none" stroke="#f5a623" stroke-width="1.5" points="0,12 8,14 16,11 24,9 32,10 40,8 48,6 60,5"/></svg>
</div>
<div class="clu-metric clu-metric-success bg-s2 border border-white/6 rounded-[10px] transition-colors hover:border-white/10" style="padding:14px 14px 13px;">
<div class="flex items-center gap-[7px] text-t2 text-[11.5px] font-medium">
<svg class="w-[13px] h-[13px]" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path d="M21 12c0 5-9 9-9 9s-9-4-9-9V5l9-3 9 3v7z"/><path d="M9 11l2 2 4-4"/></svg>
Backups 24h
</div>
<div class="font-mono text-[22px] font-semibold tracking-[-0.02em] leading-[1.1] mt-2">
86<span class="text-[13px] text-t2 font-medium ml-[2px]">/87</span>
</div>
<div class="flex items-center gap-[6px] text-t2 text-[11px] mt-[6px]">
<span class="font-mono font-medium text-warning">1 fehlgeschlagen</span> · 02:14 UTC
</div>
<svg class="absolute right-[10px] bottom-[10px] opacity-70" width="60" height="22" viewBox="0 0 60 22" aria-hidden="true"><polyline fill="none" stroke="#3ecf8e" stroke-width="1.5" points="0,6 8,5 16,6 24,5 32,6 40,5 48,6 60,5"/></svg>
</div>
<div class="clu-metric clu-metric-danger bg-s2 border border-white/6 rounded-[10px] transition-colors hover:border-white/10" style="padding:14px 14px 13px;">
<div class="flex items-center gap-[7px] text-t2 text-[11.5px] font-medium">
<svg class="w-[13px] h-[13px]" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path d="M12 3l8 4v5c0 5-3.5 8-8 9-4.5-1-8-4-8-9V7l8-4z"/><path d="M12 8v5M12 16h.01"/></svg>
Security Events
</div>
<div class="font-mono text-[22px] font-semibold tracking-[-0.02em] leading-[1.1] mt-2">128</div>
<div class="flex items-center gap-[6px] text-t2 text-[11px] mt-[6px]">
<span class="font-mono font-medium text-danger"> 23%</span> Brute-force blockiert
</div>
<svg class="absolute right-[10px] bottom-[10px] opacity-70" width="60" height="22" viewBox="0 0 60 22" aria-hidden="true"><polyline fill="none" stroke="#f05252" stroke-width="1.5" points="0,16 8,12 16,14 24,9 32,11 40,6 48,8 60,4"/></svg>
</div>
<div class="clu-metric bg-s2 border border-white/6 rounded-[10px] transition-colors hover:border-white/10" style="padding:14px 14px 13px;">
<div class="flex items-center gap-[7px] text-t2 text-[11.5px] font-medium">
<svg class="w-[13px] h-[13px]" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><ellipse cx="12" cy="6" rx="9" ry="3"/><path d="M3 6v12c0 1.7 4 3 9 3s9-1.3 9-3V6M3 12c0 1.7 4 3 9 3s9-1.3 9-3"/></svg>
Storage
</div>
<div class="font-mono text-[22px] font-semibold tracking-[-0.02em] leading-[1.1] mt-2">
1.84<span class="text-[13px] text-t2 font-medium ml-[2px]">TB</span>
</div>
<div class="flex items-center gap-[6px] text-t2 text-[11px] mt-[6px]">
<span class="font-mono font-medium text-warning">73%</span> von 2.5 TB belegt
</div>
<svg class="absolute right-[10px] bottom-[10px] opacity-70" width="60" height="22" viewBox="0 0 60 22" aria-hidden="true"><polyline fill="none" stroke="#8888a0" stroke-width="1.5" points="0,14 8,13 16,12 24,11 32,10 40,9 48,8 60,7"/></svg>
</div>
</div>
{{-- SERVER TABLE --}}
<section class="bg-s2 border border-white/6 rounded-[10px] overflow-hidden">
<div class="flex items-center justify-between border-b border-white/6" style="padding:13px 16px 12px;">
<h3 class="m-0 text-[13px] font-semibold tracking-[-0.005em] flex items-center gap-2">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><rect x="3" y="4" width="18" height="7" rx="1.5"/><rect x="3" y="13" width="18" height="7" rx="1.5"/><circle cx="7" cy="7.5" r="0.7" fill="currentColor"/><circle cx="7" cy="16.5" r="0.7" fill="currentColor"/></svg>
Server-Flotte
<span class="font-mono text-[10.5px] text-t2 border border-white/6 bg-hover px-[7px] py-[2px] rounded-[4px] ml-[6px]">6 nodes</span>
</h3>
<div class="flex gap-[2px]">
<button class="px-[9px] py-1 text-[11.5px] text-t1 rounded-[5px] bg-hover transition-colors">Alle</button>
<button class="px-[9px] py-1 text-[11.5px] text-t2 rounded-[5px] transition-colors hover:text-t1">Produktion</button>
<button class="px-[9px] py-1 text-[11.5px] text-t2 rounded-[5px] transition-colors hover:text-t1">Staging</button>
<button class="px-[9px] py-1 text-[11.5px] text-t2 rounded-[5px] transition-colors hover:text-t1">Edge</button>
</div>
</div>
<div class="overflow-x-auto">
<table class="w-full border-collapse text-[12.5px]">
<thead>
<tr>
<th class="text-left font-medium text-t2 text-[11px] tracking-[.04em] uppercase border-b border-white/6 bg-surface sticky top-0" style="padding:9px 14px;">Server</th>
<th class="text-left font-medium text-t2 text-[11px] tracking-[.04em] uppercase border-b border-white/6 bg-surface sticky top-0" style="padding:9px 14px;">Sites</th>
<th class="text-left font-medium text-t2 text-[11px] tracking-[.04em] uppercase border-b border-white/6 bg-surface sticky top-0" style="padding:9px 14px;">CPU · RAM · Disk</th>
<th class="text-left font-medium text-t2 text-[11px] tracking-[.04em] uppercase border-b border-white/6 bg-surface sticky top-0" style="padding:9px 14px;">Standort</th>
<th class="text-right font-medium text-t2 text-[11px] tracking-[.04em] uppercase border-b border-white/6 bg-surface sticky top-0" style="padding:9px 14px;">Uptime</th>
<th class="border-b border-white/6 bg-surface sticky top-0 w-[120px]" style="padding:9px 14px;"></th>
</tr>
</thead>
<tbody id="server-body"></tbody>
</table>
</div>
</section>
{{-- SITES TABLE --}}
<section class="bg-s2 border border-white/6 rounded-[10px] overflow-hidden">
<div class="flex items-center justify-between border-b border-white/6" style="padding:13px 16px 12px;">
<h3 class="m-0 text-[13px] font-semibold tracking-[-0.005em] flex items-center gap-2">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><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"/></svg>
WordPress-Sites
<span class="font-mono text-[10.5px] text-t2 border border-white/6 bg-hover px-[7px] py-[2px] rounded-[4px] ml-[6px]">8 sichtbar · 87 gesamt</span>
</h3>
<div class="flex gap-2 items-center">
<button class="px-[9px] py-[5px] text-[11.5px] text-t2 border border-white/6 rounded-md bg-surface transition-all hover:text-t1 hover:border-white/10">Filter</button>
<button class="px-[9px] py-[5px] text-[11.5px] text-t2 border border-white/6 rounded-md bg-surface transition-all hover:text-t1 hover:border-white/10">Sortieren</button>
<button class="px-[9px] py-[5px] text-[11.5px] text-t1 border border-white/10 rounded-md bg-hover transition-all hover:border-white/15">Alle anzeigen </button>
</div>
</div>
<div class="overflow-x-auto">
<table class="w-full border-collapse text-[12.5px]">
<thead>
<tr>
<th class="text-left font-medium text-t2 text-[11px] tracking-[.04em] uppercase border-b border-white/6 bg-surface sticky top-0" style="padding:9px 14px;">Site</th>
<th class="text-left font-medium text-t2 text-[11px] tracking-[.04em] uppercase border-b border-white/6 bg-surface sticky top-0" style="padding:9px 14px;">Server</th>
<th class="text-left font-medium text-t2 text-[11px] tracking-[.04em] uppercase border-b border-white/6 bg-surface sticky top-0" style="padding:9px 14px;">WP</th>
<th class="text-left font-medium text-t2 text-[11px] tracking-[.04em] uppercase border-b border-white/6 bg-surface sticky top-0" style="padding:9px 14px;">PHP</th>
<th class="text-right font-medium text-t2 text-[11px] tracking-[.04em] uppercase border-b border-white/6 bg-surface sticky top-0" style="padding:9px 14px;">Visitors / 24h</th>
<th class="text-left font-medium text-t2 text-[11px] tracking-[.04em] uppercase border-b border-white/6 bg-surface sticky top-0" style="padding:9px 14px;">Updates</th>
<th class="text-left font-medium text-t2 text-[11px] tracking-[.04em] uppercase border-b border-white/6 bg-surface sticky top-0" style="padding:9px 14px;">Backup</th>
<th class="text-left font-medium text-t2 text-[11px] tracking-[.04em] uppercase border-b border-white/6 bg-surface sticky top-0" style="padding:9px 14px;">Security</th>
<th class="border-b border-white/6 bg-surface sticky top-0" style="padding:9px 14px;"></th>
</tr>
</thead>
<tbody id="sites-body"></tbody>
</table>
</div>
</section>
{{-- BOTTOM ROW --}}
<div style="display:grid;grid-template-columns:1.4fr 1fr 1fr;gap:14px;">
{{-- Traffic chart --}}
<section class="bg-s2 border border-white/6 rounded-[10px] overflow-hidden">
<div class="flex items-center justify-between border-b border-white/6" style="padding:13px 16px 12px;">
<h3 class="m-0 text-[13px] font-semibold tracking-[-0.005em] flex items-center gap-2">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path d="M3 17l5-5 4 4 7-8M14 8h6v6"/></svg>
Traffic
<span class="inline-flex items-center gap-[5px] font-mono text-[10.5px] text-success font-medium ml-[2px]">
<span class="clu-pulse-dot w-[6px] h-[6px] rounded-full bg-current"></span>live
</span>
</h3>
<div class="flex gap-[2px]">
<button class="px-[9px] py-1 text-[11.5px] text-t2 rounded-[5px] transition-colors hover:text-t1">15m</button>
<button class="px-[9px] py-1 text-[11.5px] text-t1 rounded-[5px] bg-hover transition-colors">60m</button>
<button class="px-[9px] py-1 text-[11.5px] text-t2 rounded-[5px] transition-colors hover:text-t1">24h</button>
<button class="px-[9px] py-1 text-[11.5px] text-t2 rounded-[5px] transition-colors hover:text-t1">7d</button>
</div>
</div>
<div style="padding:14px 16px 8px;">
<div class="flex items-end justify-between mb-[6px]">
<div>
<div class="font-mono text-[22px] font-semibold tracking-[-0.02em]">14.2k<span class="text-[13px] text-t2 ml-1 font-medium">req/min</span></div>
<div class="text-[11px] text-t2 mt-[2px]">Peak vor 12 min · 16.8k req/min</div>
</div>
<div class="flex gap-[14px] text-[11px] text-t2 font-mono">
<span><i class="inline-block w-2 h-2 rounded-[2px] mr-[5px] align-middle" style="background:#4f8ef7;"></i>Requests</span>
<span><i class="inline-block w-2 h-2 rounded-[2px] mr-[5px] align-middle" style="background:#7c5cff;"></i>Cache hit</span>
</div>
</div>
<svg id="traffic-chart" class="w-full block" height="160" viewBox="0 0 600 160" preserveAspectRatio="none">
<defs>
<linearGradient id="grad-req" x1="0" x2="0" y1="0" y2="1">
<stop offset="0%" stop-color="#4f8ef7" stop-opacity="0.35"/>
<stop offset="100%" stop-color="#4f8ef7" stop-opacity="0"/>
</linearGradient>
<linearGradient id="grad-cache" x1="0" x2="0" y1="0" y2="1">
<stop offset="0%" stop-color="#7c5cff" stop-opacity="0.20"/>
<stop offset="100%" stop-color="#7c5cff" stop-opacity="0"/>
</linearGradient>
</defs>
<g stroke="rgba(255,255,255,0.04)" stroke-width="1">
<line x1="0" y1="30" x2="600" y2="30"/>
<line x1="0" y1="65" x2="600" y2="65"/>
<line x1="0" y1="100" x2="600" y2="100"/>
<line x1="0" y1="135" x2="600" y2="135"/>
</g>
<text class="clu-chart-axis" x="4" y="34">20k</text>
<text class="clu-chart-axis" x="4" y="69">15k</text>
<text class="clu-chart-axis" x="4" y="104">10k</text>
<text class="clu-chart-axis" x="4" y="139">5k</text>
<path id="cache-area" fill="url(#grad-cache)"/>
<path id="cache-line" stroke="#7c5cff" stroke-width="1.6" fill="none" stroke-linecap="round" stroke-linejoin="round"/>
<path id="req-area" fill="url(#grad-req)"/>
<path id="req-line" stroke="#4f8ef7" stroke-width="1.8" fill="none" stroke-linecap="round" stroke-linejoin="round"/>
<circle id="live-dot" cx="0" cy="0" r="3.5" fill="#4f8ef7" stroke="#0a0a0f" stroke-width="2"/>
<text class="clu-chart-axis" x="40" y="156">-60m</text>
<text class="clu-chart-axis" x="180" y="156">-45m</text>
<text class="clu-chart-axis" x="320" y="156">-30m</text>
<text class="clu-chart-axis" x="460" y="156">-15m</text>
<text class="clu-chart-axis" x="568" y="156">jetzt</text>
</svg>
</div>
</section>
{{-- Update pipeline --}}
<section class="bg-s2 border border-white/6 rounded-[10px] overflow-hidden">
<div class="flex items-center justify-between border-b border-white/6" style="padding:13px 16px 12px;">
<h3 class="m-0 text-[13px] font-semibold tracking-[-0.005em] flex items-center gap-2">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path d="M21 12a9 9 0 1 1-3-6.7M21 4v5h-5"/></svg>
Update-Pipeline
</h3>
<span class="text-[11.5px] text-t2 font-mono">5 aktiv · 19 wartend</span>
</div>
<div id="pipe-body" class="flex flex-col"></div>
</section>
{{-- Activity feed --}}
<section class="bg-s2 border border-white/6 rounded-[10px] overflow-hidden">
<div class="flex items-center justify-between border-b border-white/6" style="padding:13px 16px 12px;">
<h3 class="m-0 text-[13px] font-semibold tracking-[-0.005em] flex items-center gap-2">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path d="M22 12h-4l-3 9L9 3l-3 9H2"/></svg>
Live Activity
<span class="inline-flex items-center gap-[5px] font-mono text-[10.5px] text-success font-medium ml-[2px]">
<span class="clu-pulse-dot w-[6px] h-[6px] rounded-full bg-current"></span>live
</span>
</h3>
<span class="text-[11.5px] text-t2 font-mono">letzte 30 min</span>
</div>
<div id="feed-body" class="py-[6px]"></div>
</section>
</div>
<div class="text-center text-dim text-[11px] font-mono tracking-[.04em] pb-5">
CluPilot · region: eu-central-1 · build #4f8a32c · uptime 99.984%
</div>
</div>
</main>
</div>
<script>
/* ── Icon helpers ── */
const IC = {
terminal: '<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>',
restart: '<svg width="14" height="14" 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>',
more: '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="5" cy="12" r="1.5"/><circle cx="12" cy="12" r="1.5"/><circle cx="19" cy="12" r="1.5"/></svg>',
ext: '<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>',
};
/* ── pill helper ── */
function pill(cls, label) {
const map = {
success: 'color:#3ecf8e;background:rgba(62,207,142,0.12);border-color:rgba(62,207,142,0.22);',
warning: 'color:#f5a623;background:rgba(245,166,35,0.12);border-color:rgba(245,166,35,0.22);',
danger: 'color:#f05252;background:rgba(240,82,82,0.12);border-color:rgba(240,82,82,0.22);',
accent: 'color:#4f8ef7;background:rgba(79,142,247,0.12);border-color:rgba(79,142,247,0.22);',
muted: 'color:#8888a0;background:#1a1a24;border-color:rgba(255,255,255,0.06);',
default: 'color:#f0f0f5;background:#1a1a24;border-color:rgba(255,255,255,0.06);',
};
return `<span style="display:inline-flex;align-items:center;gap:5px;padding:2px 7px;font-size:10.5px;font-weight:500;border-radius:4px;font-family:var(--font-mono);border:1px solid;line-height:1.4;${map[cls]||map.default}">${label}</span>`;
}
/* ── res-bar helper ── */
function resBar(label, pct) {
const color = pct < 60 ? '#3ecf8e' : pct < 80 ? '#f5a623' : '#f05252';
return `<div style="display:flex;flex-direction:column;gap:3px;min-width:90px;">
<div style="display:flex;justify-content:space-between;font-size:10.5px;color:#8888a0;font-family:var(--font-mono);">
<span>${label}</span><span style="color:#f0f0f5;">${pct}%</span>
</div>
<div style="height:4px;background:#1d1d28;border-radius:2px;overflow:hidden;">
<div style="height:100%;width:${pct}%;background:${color};border-radius:2px;transition:width .3s;"></div>
</div>
</div>`;
}
/* ── SERVERS ── */
const servers = [
{name:'eu-prod-01', ip:'185.243.117.42', sites:23, cpu:42, ram:58, disk:71, loc:'Frankfurt 🇩🇪', flag:'FRA', uptime:'127d 14h', ok:true, host:'Hetzner · CX42'},
{name:'eu-prod-02', ip:'185.243.117.43', sites:19, cpu:67, ram:74, disk:55, loc:'Frankfurt 🇩🇪', flag:'FRA', uptime:'127d 14h', ok:true, host:'Hetzner · CX42'},
{name:'eu-edge-amsterdam', ip:'78.46.201.18', sites:14, cpu:31, ram:44, disk:38, loc:'Amsterdam 🇳🇱', flag:'AMS', uptime:'89d 02h', ok:true, host:'Hetzner · CX32'},
{name:'us-east-prod-01', ip:'5.161.42.198', sites:21, cpu:78, ram:81, disk:62, loc:'Ashburn 🇺🇸', flag:'IAD', uptime:'214d 06h', ok:false, host:'Hetzner · CCX23'},
{name:'eu-staging', ip:'168.119.84.221', sites:6, cpu:18, ram:29, disk:22, loc:'Nürnberg 🇩🇪', flag:'NBG', uptime:'43d 11h', ok:true, host:'Hetzner · CX22'},
{name:'apac-sydney-01', ip:'209.97.165.88', sites:4, cpu:54, ram:51, disk:84, loc:'Sydney 🇦🇺', flag:'SYD', uptime:'62d 19h', ok:false, host:'DigitalOcean'},
];
const serverBody = document.getElementById('server-body');
servers.forEach(s => {
const stateIco = s.ok
? '<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M5 12l5 5L20 7"/></svg>'
: '<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 9v4M12 17h.01"/><circle cx="12" cy="12" r="9"/></svg>';
const iconStyle = s.ok
? 'color:#3ecf8e;background:rgba(62,207,142,0.06);border-color:rgba(62,207,142,0.18);'
: 'color:#f5a623;background:rgba(245,166,35,0.06);border-color:rgba(245,166,35,0.18);';
const tr = document.createElement('tr');
tr.style.cssText = 'transition:background .12s;';
tr.onmouseenter = () => tr.style.background = '#1a1a24';
tr.onmouseleave = () => tr.style.background = '';
tr.innerHTML = `
<td style="padding:11px 14px;border-bottom:1px solid rgba(255,255,255,.06);vertical-align:middle;">
<div style="display:flex;align-items:center;gap:10px;">
<div style="width:30px;height:30px;border-radius:7px;border:1px solid;display:grid;place-items:center;flex:none;${iconStyle}">${stateIco}</div>
<div>
<div style="font-weight:500;color:#f0f0f5;font-size:12.5px;">${s.name}</div>
<div style="font-family:var(--font-mono);font-size:10.5px;color:#8888a0;">${s.ip} · ${s.host}</div>
</div>
</div>
</td>
<td style="padding:11px 14px;border-bottom:1px solid rgba(255,255,255,.06);vertical-align:middle;">${pill('default', s.sites + ' sites')}</td>
<td style="padding:11px 14px;border-bottom:1px solid rgba(255,255,255,.06);vertical-align:middle;">
<div style="display:grid;grid-template-columns:repeat(3,minmax(80px,1fr));gap:10px;min-width:300px;">
${resBar('CPU', s.cpu)}${resBar('RAM', s.ram)}${resBar('Disk', s.disk)}
</div>
</td>
<td style="padding:11px 14px;border-bottom:1px solid rgba(255,255,255,.06);vertical-align:middle;">
<span style="color:#f0f0f5;">${s.loc.replace(/(.*) (.*)/, '$1')}</span>
${pill('muted', s.flag)}
</td>
<td style="padding:11px 14px;border-bottom:1px solid rgba(255,255,255,.06);vertical-align:middle;text-align:right;font-family:var(--font-mono);">
<span style="color:#f0f0f5;">${s.uptime}</span>
</td>
<td style="padding:11px 14px;border-bottom:1px solid rgba(255,255,255,.06);vertical-align:middle;text-align:right;">
<div class="clu-row-actions">
${['SSH-Terminal','Neustart','Öffnen','Mehr'].map((t,i)=>`<button title="${t}" style="width:26px;height:26px;border-radius:5px;color:#8888a0;display:grid;place-items:center;transition:all .15s;cursor:pointer;" onmouseenter="this.style.color='#f0f0f5';this.style.background='#22222e'" onmouseleave="this.style.color='#8888a0';this.style.background=''">${[IC.terminal,IC.restart,IC.ext,IC.more][i]}</button>`).join('')}
</div>
</td>`;
serverBody.appendChild(tr);
});
/* ── SITES ── */
const palette = ['#4f8ef7','#3ecf8e','#f5a623','#7c5cff','#f05252','#22c1d6','#e879f9','#facc15'];
const sites = [
{url:'kunde-mueller.de', srv:'eu-prod-01', wp:'6.5.2', wpOk:true, php:'8.2', vis:'12.4k', upd:0, bk:'ok', grade:'A'},
{url:'shop.bergmann-coffee.com', srv:'eu-prod-01', wp:'6.4.3', wpOk:false, php:'8.1', vis:'48.7k', upd:3, bk:'ok', grade:'A'},
{url:'blog.fintech-radar.io', srv:'eu-prod-02', wp:'6.5.2', wpOk:true, php:'8.3', vis:'9.1k', upd:1, bk:'ok', grade:'B'},
{url:'praxis-dr-schwarz.de', srv:'eu-prod-02', wp:'6.2.1', wpOk:false, php:'7.4', vis:'1.2k', upd:7, bk:'warn', grade:'D'},
{url:'agentur-helio.com', srv:'eu-edge-amsterdam', wp:'6.5.2', wpOk:true, php:'8.2', vis:'18.2k', upd:0, bk:'ok', grade:'A'},
{url:'docs.opencrate.dev', srv:'us-east-prod-01', wp:'6.5.1', wpOk:true, php:'8.3', vis:'7.8k', upd:2, bk:'ok', grade:'B'},
{url:'staging.opencrate.dev', srv:'eu-staging', wp:'6.6-RC1',wpOk:true, php:'8.3', vis:'82', upd:0, bk:'ok', grade:'B'},
{url:'classic.tonhoff-bau.de', srv:'us-east-prod-01', wp:'5.9.7', wpOk:false, php:'7.4', vis:'3.4k', upd:11, bk:'err', grade:'F'},
];
const sitesBody = document.getElementById('sites-body');
const gradeStyle = {
A:'color:#3ecf8e;background:rgba(62,207,142,0.15);box-shadow:0 0 0 1px rgba(62,207,142,0.25) inset;',
B:'color:#4f8ef7;background:rgba(79,142,247,0.15);box-shadow:0 0 0 1px rgba(79,142,247,0.25) inset;',
C:'color:#f5a623;background:rgba(245,166,35,0.15);box-shadow:0 0 0 1px rgba(245,166,35,0.25) inset;',
D:'color:#f5a623;background:rgba(245,166,35,0.18);box-shadow:0 0 0 1px rgba(245,166,35,0.32) inset;',
F:'color:#f05252;background:rgba(240,82,82,0.15);box-shadow:0 0 0 1px rgba(240,82,82,0.28) inset;',
};
const bkMap = {
ok: {cls:'success', label:'vor 2 h'},
warn: {cls:'warning', label:'vor 18 h'},
err: {cls:'danger', label:'fehlgeschl.'},
};
sites.forEach((s, i) => {
const color = palette[i % palette.length];
const updCls = s.upd === 0 ? 'success' : s.upd >= 5 ? 'danger' : 'warning';
const updLbl = s.upd === 0 ? 'aktuell' : s.upd + ' verfügbar';
const bk = bkMap[s.bk];
const tr = document.createElement('tr');
tr.style.cssText = 'transition:background .12s;';
tr.onmouseenter = () => tr.style.background = '#1a1a24';
tr.onmouseleave = () => tr.style.background = '';
tr.innerHTML = `
<td style="padding:11px 14px;border-bottom:1px solid rgba(255,255,255,.06);vertical-align:middle;">
<div style="display:flex;align-items:center;gap:10px;min-width:0;">
<div style="width:24px;height:24px;border-radius:5px;display:grid;place-items:center;font-weight:600;font-size:11px;color:#0a0a0f;flex:none;font-family:var(--font-mono);background:${color};">${s.url[0].toUpperCase()}</div>
<div style="min-width:0;">
<div style="font-weight:500;color:#f0f0f5;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:200px;">${s.url}</div>
<div style="font-family:var(--font-mono);font-size:10.5px;color:#8888a0;">id_${(7300+i).toString(16)} · ssl </div>
</div>
</div>
</td>
<td style="padding:11px 14px;border-bottom:1px solid rgba(255,255,255,.06);vertical-align:middle;">${pill('muted', s.srv)}</td>
<td style="padding:11px 14px;border-bottom:1px solid rgba(255,255,255,.06);vertical-align:middle;">${pill(s.wpOk?'success':'warning', 'WP '+s.wp)}</td>
<td style="padding:11px 14px;border-bottom:1px solid rgba(255,255,255,.06);vertical-align:middle;">${pill(parseFloat(s.php)<8?'warning':'muted', 'PHP '+s.php)}</td>
<td style="padding:11px 14px;border-bottom:1px solid rgba(255,255,255,.06);vertical-align:middle;text-align:right;font-family:var(--font-mono);color:#f0f0f5;">${s.vis}</td>
<td style="padding:11px 14px;border-bottom:1px solid rgba(255,255,255,.06);vertical-align:middle;">${pill(updCls, updLbl)}</td>
<td style="padding:11px 14px;border-bottom:1px solid rgba(255,255,255,.06);vertical-align:middle;">${pill(bk.cls, bk.label)}</td>
<td style="padding:11px 14px;border-bottom:1px solid rgba(255,255,255,.06);vertical-align:middle;">
<div style="display:inline-grid;place-items:center;width:24px;height:24px;border-radius:6px;font-family:var(--font-mono);font-weight:600;font-size:12px;line-height:1;${gradeStyle[s.grade]}">${s.grade}</div>
</td>
<td style="padding:11px 14px;border-bottom:1px solid rgba(255,255,255,.06);vertical-align:middle;text-align:right;">
<div class="clu-row-actions">
${['Site öffnen','WP-Admin','Mehr'].map((t,j)=>`<button title="${t}" style="width:26px;height:26px;border-radius:5px;color:#8888a0;display:grid;place-items:center;transition:all .15s;cursor:pointer;" onmouseenter="this.style.color='#f0f0f5';this.style.background='#22222e'" onmouseleave="this.style.color='#8888a0';this.style.background=''">${[IC.ext,IC.terminal,IC.more][j]}</button>`).join('')}
</div>
</td>`;
sitesBody.appendChild(tr);
});
/* ── PIPELINE ── */
const pipeItems = [
{type:'WP', cls:'wp', title:'WordPress 6.5.2 → 6.5.3', sub:'agentur-helio.com', pct:78, status:'läuft', sCls:'accent'},
{type:'PL', cls:'pl', title:'WooCommerce 8.7 → 8.8.1', sub:'shop.bergmann-coffee.com', pct:42, status:'läuft', sCls:'accent'},
{type:'PL', cls:'pl', title:'Yoast SEO 22.4 → 22.5', sub:'blog.fintech-radar.io', pct:100, status:'erledigt', sCls:'success'},
{type:'PHP',cls:'php', title:'PHP 7.4 → 8.2 Migration', sub:'praxis-dr-schwarz.de', pct:12, status:'review', sCls:'warning'},
{type:'WP', cls:'wp', title:'WordPress 5.9 → 6.5.2', sub:'classic.tonhoff-bau.de', pct:0, status:'fehler', sCls:'danger'},
{type:'PL', cls:'pl', title:'Wordfence 7.11 → 7.12.0', sub:'+ 14 sites batch', pct:0, status:'wartet', sCls:'muted'},
];
const icoClsMap = {
wp: 'color:#21759b;border-color:rgba(33,117,155,0.3);background:rgba(33,117,155,0.08);',
pl: 'color:#4f8ef7;border-color:rgba(79,142,247,0.25);background:rgba(79,142,247,0.12);',
php: 'color:#7c5cff;border-color:rgba(124,92,255,0.28);background:rgba(124,92,255,0.10);',
};
const barColor = {accent:'#4f8ef7', success:'#3ecf8e', warning:'#f5a623', danger:'#f05252'};
const pipeBody = document.getElementById('pipe-body');
pipeItems.forEach(p => {
const div = document.createElement('div');
div.style.cssText = 'display:flex;align-items:center;gap:11px;padding:10px 16px;border-bottom:1px solid rgba(255,255,255,.06);transition:background .12s;cursor:default;';
div.onmouseenter = () => div.style.background = '#1a1a24';
div.onmouseleave = () => div.style.background = '';
div.innerHTML = `
<div style="width:26px;height:26px;border-radius:6px;display:grid;place-items:center;flex:none;border:1px solid;font-size:10.5px;font-family:var(--font-mono);font-weight:600;${icoClsMap[p.cls]}">${p.type}</div>
<div style="flex:1;min-width:0;">
<div style="font-size:12.5px;font-weight:500;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;">${p.title}</div>
<div style="font-size:11px;color:#8888a0;font-family:var(--font-mono);">${p.sub}</div>
</div>
<div style="display:flex;flex-direction:column;align-items:flex-end;gap:4px;">
${pill(p.sCls, (p.pct>0&&p.pct<100?p.pct+'% ':'')+p.status)}
${p.pct>0?`<div style="width:60px;height:4px;background:#1d1d28;border-radius:2px;overflow:hidden;"><div style="height:100%;width:${p.pct}%;background:${barColor[p.sCls]||'#4f8ef7'};border-radius:2px;"></div></div>`:''}
</div>`;
pipeBody.appendChild(div);
});
/* ── ACTIVITY FEED ── */
const feedItems = [
{ic:'success', title:'Backup erstellt für', target:'agentur-helio.com', meta:'vor 12 sek · 248 MB · eu-prod-02',
icon:'<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2"><path d="M5 12l5 5L20 7"/></svg>'},
{ic:'danger', title:'17 Login-Versuche blockiert auf', target:'classic.tonhoff-bau.de', meta:'vor 1 min · IP 185.220.101.18 · TOR exit',
icon:'<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2"><path d="M12 3l8 4v5c0 5-3.5 8-8 9-4.5-1-8-4-8-9V7l8-4z"/></svg>'},
{ic:'accent', title:'Plugin-Update Yoast SEO 22.5 auf 3 Sites', target:null, meta:'vor 4 min · ausgeführt von m.weber',
icon:'<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2"><path d="M21 12a9 9 0 1 1-3-6.7M21 4v5h-5"/></svg>'},
{ic:'warning', title:'CPU > 80% auf', target:'us-east-prod-01', meta:'vor 7 min · sustained 4m 12s',
icon:'<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2"><path d="M12 9v4M12 17h.01"/><circle cx="12" cy="12" r="9"/></svg>'},
{ic:'success', title:'SSL-Zertifikat erneuert ·', target:'*.kunde-mueller.de', meta:'vor 14 min · gültig bis 25.08.2026',
icon:'<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2"><rect x="5" y="11" width="14" height="9" rx="1.5"/><path d="M8 11V7a4 4 0 0 1 8 0v4"/></svg>'},
{ic:'accent', title:'SSH-Session geöffnet auf', target:'eu-prod-01', meta:'vor 19 min · user j.koch · key id_ed25519_jk',
icon:'<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2"><path d="M4 17l5-5-5-5M13 19h7"/></svg>'},
{ic:'success', title:'Health-Check OK · 87/87 Sites', target:null, meta:'vor 23 min · run #4421',
icon:'<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2"><path d="M5 12l5 5L20 7"/></svg>'},
];
const icoStyle = {
success: 'color:#3ecf8e;background:rgba(62,207,142,0.12);border-color:rgba(62,207,142,0.20);',
warning: 'color:#f5a623;background:rgba(245,166,35,0.12);border-color:rgba(245,166,35,0.20);',
danger: 'color:#f05252;background:rgba(240,82,82,0.12);border-color:rgba(240,82,82,0.20);',
accent: 'color:#4f8ef7;background:rgba(79,142,247,0.12);border-color:rgba(79,142,247,0.22);',
};
const feedBody = document.getElementById('feed-body');
feedItems.forEach(f => {
const targetHtml = f.target
? ` <span style="font-family:var(--font-mono);color:#f0f0f5;background:#1d1d28;padding:1px 5px;border-radius:3px;font-size:11px;border:1px solid rgba(255,255,255,0.06);">${f.target}</span>`
: '';
const div = document.createElement('div');
div.style.cssText = 'display:flex;gap:11px;padding:10px 16px;align-items:flex-start;transition:background .12s;cursor:default;';
div.onmouseenter = () => div.style.background = '#1a1a24';
div.onmouseleave = () => div.style.background = '';
div.innerHTML = `
<div style="width:24px;height:24px;border-radius:6px;display:grid;place-items:center;flex:none;border:1px solid;${icoStyle[f.ic]}">${f.icon}</div>
<div style="flex:1;min-width:0;font-size:12px;">
<div style="color:#8888a0;">${f.title}${targetHtml}</div>
<div style="color:#8888a0;font-size:10.5px;margin-top:2px;font-family:var(--font-mono);">${f.meta}</div>
</div>`;
feedBody.appendChild(div);
});
/* ── TRAFFIC CHART ── */
(function(){
const W=600,H=160,padL=30,padR=8,padT=10,padB=28,N=60;
const req=[],cache=[];
let v=11;
for(let i=0;i<N;i++){
v+=(Math.random()-.45)*1.6;
if(i>40&&i<52) v+=Math.sin((i-40)/12*Math.PI)*3.5;
v=Math.max(5,Math.min(19,v));
req.push(v);
cache.push(v*(.55+Math.random()*.15));
}
const xc=i=>padL+(W-padL-padR)*(i/(N-1));
const yc=v=>{const t=v/20;return H-padB-t*(H-padT-padB);};
function pathFor(arr){
let d=`M ${xc(0)} ${yc(arr[0])}`;
for(let i=1;i<arr.length;i++){
const cx=(xc(i-1)+xc(i))/2;
d+=` Q ${xc(i-1)} ${yc(arr[i-1])} ${cx} ${(yc(arr[i-1])+yc(arr[i]))/2}`;
}
return d+` T ${xc(arr.length-1)} ${yc(arr[arr.length-1])}`;
}
function areaFor(arr){return pathFor(arr)+` L ${xc(arr.length-1)} ${H-padB} L ${xc(0)} ${H-padB} Z`;}
document.getElementById('req-line').setAttribute('d',pathFor(req));
document.getElementById('req-area').setAttribute('d',areaFor(req));
document.getElementById('cache-line').setAttribute('d',pathFor(cache));
document.getElementById('cache-area').setAttribute('d',areaFor(cache));
const dot=document.getElementById('live-dot');
dot.setAttribute('cx',xc(N-1));dot.setAttribute('cy',yc(req[N-1]));
let t0=0;
(function tick(){t0+=.04;dot.setAttribute('r',3.5+Math.sin(t0)*.8);requestAnimationFrame(tick);})();
})();
</script>
@endsection

View File

@ -12,7 +12,7 @@
<link href="https://fonts.bunny.net/css?family=figtree:400,500,600&display=swap" rel="stylesheet" />
<!-- Scripts -->
@vite(['resources/css/app.css', 'resources/js/app.js'])
@vite(['resources/css/app.css'])
</head>
<body class="font-sans antialiased">
<div class="min-h-screen bg-gray-100">
@ -32,6 +32,7 @@
{{ $slot }}
</main>
</div>
<livewire:livewire-ui-modal />
@vite(['resources/js/app.js'])
@livewire('wire-elements-modal')
</body>
</html>

View File

@ -0,0 +1,18 @@
<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="csrf-token" content="{{ csrf_token() }}">
<title>{{ config('app.name', 'CluPilot') }}</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet">
@vite(['resources/css/app.css', 'resources/js/app.js'])
@livewireStyles
</head>
<body class="bg-bg text-t1 font-sans antialiased" style="font-size:13px;line-height:1.45;">
@yield('content')
@livewireScripts
</body>
</html>

View File

@ -0,0 +1,18 @@
<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="csrf-token" content="{{ csrf_token() }}">
<title>Anmelden · {{ config('app.name', 'CluPilot') }}</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet">
@vite(['resources/css/app.css', 'resources/js/app.js'])
@livewireStyles
</head>
<body class="clu-login-body bg-bg text-t1 font-sans antialiased min-h-screen overflow-x-hidden" style="font-size:13.5px;line-height:1.5;">
{{ $slot }}
@livewireScripts
</body>
</html>

View File

@ -5,7 +5,7 @@ use Illuminate\Support\Facades\Session;
use Livewire\Attributes\Layout;
use Livewire\Volt\Component;
new #[Layout('layouts.guest')] class extends Component
new #[Layout('layouts.login')] class extends Component
{
public LoginForm $form;
@ -18,61 +18,249 @@ new #[Layout('layouts.guest')] class extends Component
}
}; ?>
<div>
<x-auth-session-status class="mb-4" :status="session('status')" />
<div class="clu-login-grid">
<h2 style="font-size:1.25rem;font-weight:600;color:#f0f0f5;margin-bottom:1.5rem;">Sign in to CluPilot</h2>
{{-- ===== LEFT: FORM ===== --}}
<section class="clu-form-side flex flex-col min-h-screen">
<form wire:submit="login">
<div>
<label for="email" style="display:block;font-size:0.875rem;color:#8888a0;margin-bottom:0.375rem;">Email</label>
<input wire:model="form.email" id="email" type="email" name="email" required autofocus autocomplete="username"
style="width:100%;padding:0.5rem 0.75rem;background-color:#111117;border:1px solid rgba(255,255,255,0.06);border-radius:0.5rem;color:#f0f0f5;outline:none;"
onfocus="this.style.borderColor='#4f8ef7'" onblur="this.style.borderColor='rgba(255,255,255,0.06)'" />
<x-input-error :messages="$errors->get('form.email')" class="mt-2" />
{{-- Top row --}}
<div class="flex items-center justify-between gap-4 flex-wrap gap-y-2">
<div class="flex items-center gap-[10px] shrink-0">
<div class="clu-brand-mark w-[30px] h-[30px] rounded-[8px]" aria-hidden="true"></div>
<div class="font-semibold text-[15px] tracking-[-0.01em]">
CluPilot<span class="text-t2 font-medium ml-[5px] text-[11.5px]">v2.4</span>
</div>
</div>
<span class="clu-login-status inline-flex items-center gap-[6px] px-[9px] py-1 border border-success/25 bg-success/12 text-success rounded-full text-[11px] font-medium font-mono">
<span class="clu-pulse-dot w-[6px] h-[6px] rounded-full bg-current"></span>
All systems operational
</span>
</div>
<div style="margin-top:1rem;">
<label for="password" style="display:block;font-size:0.875rem;color:#8888a0;margin-bottom:0.375rem;">Password</label>
<input wire:model="form.password" id="password" type="password" name="password" required autocomplete="current-password"
style="width:100%;padding:0.5rem 0.75rem;background-color:#111117;border:1px solid rgba(255,255,255,0.06);border-radius:0.5rem;color:#f0f0f5;outline:none;"
onfocus="this.style.borderColor='#4f8ef7'" onblur="this.style.borderColor='rgba(255,255,255,0.06)'" />
<x-input-error :messages="$errors->get('form.password')" class="mt-2" />
{{-- Form wrap --}}
<div class="my-auto flex flex-col gap-6 w-full py-6 sm:py-10 mx-auto" style="max-width:380px;">
<header>
<h1 class="m-0 text-[24px] font-semibold tracking-[-0.02em] leading-[1.2]">Willkommen zurück</h1>
<p class="mt-[6px] text-[13.5px] text-t2">
Bei CluPilot anmelden oder
@if(Route::has('register'))
<a href="{{ route('register') }}" wire:navigate class="text-akzent font-medium hover:underline">neues Konto erstellen</a>
@endif
</p>
</header>
{{-- SSO --}}
<div class="clu-sso-row">
<button class="clu-sso-btn" type="button">
<svg width="15" height="15" viewBox="0 0 24 24" aria-hidden="true">
<path fill="#4285F4" d="M22.5 12.27c0-.79-.07-1.54-.2-2.27H12v4.51h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.32z"/>
<path fill="#34A853" d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.99.66-2.25 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84A11 11 0 0 0 12 23z"/>
<path fill="#FBBC05" d="M5.84 14.10A6.6 6.6 0 0 1 5.5 12c0-.73.13-1.44.34-2.10V7.06H2.18A11 11 0 0 0 1 12c0 1.77.42 3.45 1.18 4.94l3.66-2.84z"/>
<path fill="#EA4335" d="M12 5.38c1.62 0 3.07.56 4.21 1.64l3.15-3.15C17.45 2.10 14.96 1 12 1A11 11 0 0 0 2.18 7.06l3.66 2.84C6.71 7.30 9.14 5.38 12 5.38z"/>
</svg>
Google
</button>
<button class="clu-sso-btn" type="button">
<svg width="15" height="15" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
<path d="M12 .5C5.65.5.5 5.65.5 12c0 5.08 3.29 9.39 7.86 10.91.58.11.79-.25.79-.55v-2.02c-3.2.7-3.87-1.36-3.87-1.36-.52-1.33-1.27-1.68-1.27-1.68-1.04-.71.08-.7.08-.7 1.15.08 1.76 1.18 1.76 1.18 1.02 1.75 2.69 1.24 3.35.95.1-.74.4-1.24.73-1.53-2.55-.29-5.24-1.28-5.24-5.69 0-1.26.45-2.28 1.18-3.09-.12-.29-.51-1.46.11-3.05 0 0 .97-.31 3.18 1.18.92-.26 1.91-.39 2.89-.39.98 0 1.97.13 2.89.39 2.20-1.49 3.17-1.18 3.17-1.18.63 1.59.23 2.76.12 3.05.74.81 1.18 1.83 1.18 3.09 0 4.42-2.69 5.40-5.25 5.68.41.36.78 1.06.78 2.14v3.18c0 .31.21.67.79.55C20.21 21.39 23.5 17.08 23.5 12c0-6.35-5.15-11.5-11.5-11.5z"/>
</svg>
GitHub
</button>
</div>
<div style="margin-top:1rem;">
<label for="remember" style="display:inline-flex;align-items:center;gap:0.5rem;font-size:0.875rem;color:#8888a0;">
<input wire:model="form.remember" id="remember" type="checkbox" name="remember"
style="border-radius:0.25rem;border-color:rgba(255,255,255,0.06);background-color:#111117;" />
Remember me
<div class="clu-divider">oder mit E-Mail</div>
{{-- Static dev hint --}}
<div class="flex items-start gap-[9px] px-[12px] py-[10px] rounded-[8px] border border-akzent/20 bg-akzent/8 text-[12px] text-t2">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="text-akzent flex-none mt-[1px]" aria-hidden="true">
<circle cx="12" cy="12" r="10"/><path d="M12 16v-4M12 8h.01"/>
</svg>
<span>Testinstanz &mdash; <span class="font-mono text-t1">admin@clupilot.test</span> / <span class="font-mono text-t1">clupilot</span></span>
</div>
{{-- Session status --}}
<x-auth-session-status :status="session('status')" />
{{-- Form --}}
<form wire:submit="login" class="flex flex-col gap-[14px]">
{{-- Email --}}
<div class="flex flex-col gap-[6px]">
<label for="email" class="text-[11.5px] text-t2 font-medium">E-Mail-Adresse</label>
<div class="clu-input">
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="text-dim flex-none" aria-hidden="true">
<rect x="3" y="5" width="18" height="14" rx="2"/><path d="M3 7l9 6 9-6"/>
</svg>
<input wire:model="form.email" id="email" name="email" type="text"
placeholder="du@firma.de" autocomplete="email" autofocus />
</div>
<x-input-error :messages="$errors->get('form.email')" class="mt-1" />
</div>
{{-- Password --}}
<div class="flex flex-col gap-[6px]">
<label for="password" class="flex items-center justify-between text-[11.5px] text-t2 font-medium">
Passwort
@if(Route::has('password.request'))
<a href="{{ route('password.request') }}" wire:navigate
class="text-[11.5px] text-t2 font-medium no-underline transition-colors hover:text-t1">Vergessen?</a>
@endif
</label>
<div class="clu-input">
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="text-dim flex-none" aria-hidden="true">
<rect x="5" y="11" width="14" height="9" rx="2"/><path d="M8 11V7a4 4 0 0 1 8 0v4"/>
</svg>
<input wire:model="form.password" id="password" name="password" type="password"
placeholder="••••••••••••" autocomplete="current-password" />
<button type="button" id="toggle-eye" aria-label="Passwort anzeigen"
class="text-t2 p-[4px] rounded-[4px] grid place-items-center transition-colors hover:text-t1 hover:bg-surface2">
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M1 12s4-7 11-7 11 7 11 7-4 7-11 7S1 12 1 12z"/><circle cx="12" cy="12" r="3"/>
</svg>
</button>
</div>
<x-input-error :messages="$errors->get('form.password')" class="mt-1" />
</div>
{{-- Remember (CSS peer toggle) --}}
<div class="flex items-center mt-[2px]">
<label class="flex items-center gap-2 text-[12.5px] text-t2 cursor-pointer select-none transition-colors hover:text-t1">
<input wire:model="form.remember" type="checkbox" class="peer sr-only" />
<span class="peer-checked:bg-akzent peer-checked:border-akzent flex-none w-[15px] h-[15px] rounded-[4px] border border-white/10 bg-surface grid place-items-center transition-all">
<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="3.5" stroke-linecap="round" stroke-linejoin="round"
class="opacity-0 scale-[.6] peer-checked:opacity-100 peer-checked:scale-100 transition-all duration-150">
<path d="M5 12l5 5L20 7"/>
</svg>
</span>
<span>30 Tage angemeldet bleiben</span>
</label>
</div>
<div style="display:flex;align-items:center;justify-content:space-between;margin-top:1.5rem;">
@if (Route::has('password.request'))
<a href="{{ route('password.request') }}" wire:navigate
style="font-size:0.875rem;color:#4f8ef7;text-decoration:none;"
onmouseover="this.style.opacity='.8'" onmouseout="this.style.opacity='1'">
Forgot password?
</a>
@endif
{{-- Submit --}}
<button class="clu-btn-primary" type="submit" wire:loading.attr="disabled">
<svg wire:loading class="clu-spin" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" aria-hidden="true">
<path d="M21 12a9 9 0 1 1-6.2-8.55"/>
</svg>
<span wire:loading.remove>Anmelden</span>
<span wire:loading>Authentifiziere…</span>
<svg class="arrow" wire:loading.remove width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" aria-hidden="true">
<path d="M5 12h14M13 5l7 7-7 7"/>
</svg>
</button>
<button type="submit"
style="padding:0.5rem 1.5rem;background-color:#4f8ef7;color:#fff;border:none;border-radius:0.5rem;font-weight:500;cursor:pointer;"
onmouseover="this.style.opacity='.9'" onmouseout="this.style.opacity='1'">
Sign in
{{-- Alt options --}}
<div class="flex flex-col gap-2 mt-1">
<button class="clu-alt-btn" type="button">
<svg class="text-t2 flex-none" width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true">
<path d="M22 2L11 13"/><path d="M22 2l-7 20-4-9-9-4 20-7z"/>
</svg>
Magic-Link per E-Mail senden
</button>
<button class="clu-alt-btn" type="button">
<svg class="text-t2 flex-none" width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true">
<path d="M21 12c0-4.97-4.03-9-9-9-2.5 0-4.76 1.02-6.39 2.67M3 12c0 4.97 4.03 9 9 9 2.5 0 4.76-1.02 6.39-2.67"/>
<path d="M9 12a3 3 0 1 0 6 0 3 3 0 1 0-6 0M12 1v4M12 19v4"/>
</svg>
Mit Passkey anmelden
<span class="ml-auto font-mono text-[10px] text-akzent bg-akzent/12 px-[6px] py-[2px] rounded-[4px] border border-akzent/25 font-medium">empfohlen</span>
</button>
</div>
</form>
@if (Route::has('register'))
<p style="margin-top:1.5rem;text-align:center;font-size:0.875rem;color:#8888a0;">
No account?
<a href="{{ route('register') }}" wire:navigate
style="color:#4f8ef7;text-decoration:none;"
onmouseover="this.style.opacity='.8'" onmouseout="this.style.opacity='1'">
Create one
</a>
</form>
</div>
{{-- Footer --}}
<footer class="flex items-center justify-between gap-3 text-t2 text-[11.5px] font-mono pt-3 border-t border-white/6 flex-wrap">
<div class="flex items-center gap-[6px]">
<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" aria-hidden="true">
<rect x="5" y="11" width="14" height="9" rx="2"/><path d="M8 11V7a4 4 0 0 1 8 0v4"/>
</svg>
TLS 1.3 · AES-256 · region eu-central-1
</div>
<div class="flex gap-[14px]">
<a href="#" class="text-t2 hover:text-t1 no-underline transition-colors">Datenschutz</a>
<a href="#" class="text-t2 hover:text-t1 no-underline transition-colors">AGB</a>
<a href="#" class="text-t2 hover:text-t1 no-underline transition-colors">Status</a>
<a href="#" class="text-t2 hover:text-t1 no-underline transition-colors">Hilfe</a>
</div>
</footer>
</section>
{{-- ===== RIGHT: LIVE PANEL ===== --}}
<aside class="clu-live-side relative border-l border-white/6 overflow-hidden flex flex-col gap-[18px] min-h-screen"
style="background:linear-gradient(180deg,#0c0c12 0%,#0a0a0f 100%);padding:32px 36px;"
aria-hidden="true">
<div class="relative flex items-center justify-between">
<div class="text-[11px] text-t2 font-mono uppercase tracking-[.10em]"> live cluster</div>
<div class="text-[11px] text-t2 font-mono">eu-central-1 · build #4f8a32c</div>
</div>
<div class="relative">
<p class="m-0 text-[22px] font-semibold tracking-[-0.015em] leading-[1.3]" style="max-width:480px;">
Verwalte <em class="not-italic text-akzent">87 WordPress-Sites</em> auf 6 Servern. Ein Login. Volle Kontrolle.
</p>
@endif
<p class="text-t2 text-[13px] mt-[6px]" style="max-width:460px;">
SSH-Tunnel, Backups, Updates und Security in einer Konsole von Push-to-Deploy bis Brute-Force-Defense.
</p>
</div>
{{-- Stat strip --}}
<div class="relative grid grid-cols-3 gap-[10px] mt-[6px]">
<div class="bg-s2 border border-white/6 rounded-[9px] p-3">
<div class="text-[10.5px] text-t2 uppercase tracking-[.06em] font-medium">Sites Online</div>
<div class="font-mono text-[18px] font-semibold tracking-[-0.01em] mt-1">
87<span class="text-[12px] text-t2 font-medium ml-[2px]">/87</span>
</div>
<div class="flex items-center gap-1 font-mono text-[10.5px] text-success mt-1"> 100% Uptime · 24h</div>
</div>
<div class="bg-s2 border border-white/6 rounded-[9px] p-3">
<div class="text-[10.5px] text-t2 uppercase tracking-[.06em] font-medium">Ø Response</div>
<div class="font-mono text-[18px] font-semibold tracking-[-0.01em] mt-1">
142<span class="text-[12px] text-t2 font-medium ml-[2px]">ms</span>
</div>
<div class="flex items-center gap-1 font-mono text-[10.5px] text-success mt-1"> 8.4% vs. 7d</div>
</div>
<div class="bg-s2 border border-white/6 rounded-[9px] p-3">
<div class="text-[10.5px] text-t2 uppercase tracking-[.06em] font-medium">Blocked / 24h</div>
<div class="font-mono text-[18px] font-semibold tracking-[-0.01em] mt-1">128</div>
<div class="flex items-center gap-1 font-mono text-[10.5px] text-warning mt-1"> 23% Brute-force</div>
</div>
</div>
{{-- Terminal card --}}
<div class="relative bg-s2 border border-white/6 rounded-[10px] overflow-hidden flex-1 flex flex-col" style="min-height:280px;">
<div class="flex items-center gap-[10px] border-b border-white/6 bg-surface" style="padding:10px 14px;">
<div class="flex gap-[5px]">
<span class="w-[9px] h-[9px] rounded-full bg-danger"></span>
<span class="w-[9px] h-[9px] rounded-full bg-warning"></span>
<span class="w-[9px] h-[9px] rounded-full bg-success"></span>
</div>
<div class="font-mono text-[11.5px] text-t2 flex-1 text-center">ssh deploy@eu-prod-01</div>
<div class="font-mono text-[10.5px] text-dim">pts/0</div>
</div>
<div id="term-body" class="font-mono text-[11.5px] leading-[1.6] flex-1 overflow-hidden relative" style="padding:14px 16px;"></div>
</div>
{{-- Live tags --}}
<div class="relative flex flex-wrap gap-[6px]">
<span class="inline-flex items-center gap-[5px] px-[7px] py-[3px] text-[10.5px] font-mono text-t2 bg-surface border border-white/6 rounded-[4px]">
<span class="w-[5px] h-[5px] rounded-full bg-success"></span>eu-prod-01 · 23 sites
</span>
<span class="inline-flex items-center gap-[5px] px-[7px] py-[3px] text-[10.5px] font-mono text-t2 bg-surface border border-white/6 rounded-[4px]">
<span class="w-[5px] h-[5px] rounded-full bg-success"></span>eu-prod-02 · 19 sites
</span>
<span class="inline-flex items-center gap-[5px] px-[7px] py-[3px] text-[10.5px] font-mono text-t2 bg-surface border border-white/6 rounded-[4px]">
<span class="w-[5px] h-[5px] rounded-full bg-success"></span>edge-ams · 14 sites
</span>
<span class="inline-flex items-center gap-[5px] px-[7px] py-[3px] text-[10.5px] font-mono text-t2 bg-surface border border-white/6 rounded-[4px]">
<span class="w-[5px] h-[5px] rounded-full bg-warning"></span>us-east-01 · CPU 78%
</span>
<span class="inline-flex items-center gap-[5px] px-[7px] py-[3px] text-[10.5px] font-mono text-t2 bg-surface border border-white/6 rounded-[4px]">
<span class="w-[5px] h-[5px] rounded-full bg-success"></span>staging · 6 sites
</span>
</div>
</aside>
</div>

View File

@ -8,79 +8,320 @@ use Illuminate\Validation\Rules;
use Livewire\Attributes\Layout;
use Livewire\Volt\Component;
new #[Layout('layouts.guest')] class extends Component
new #[Layout('layouts.login')] class extends Component
{
public string $name = '';
public string $firstname = '';
public string $lastname = '';
public string $email = '';
public string $workspace = '';
public string $password = '';
public string $password_confirmation = '';
public bool $terms = false;
public function register(): void
{
$validated = $this->validate([
'name' => ['required', 'string', 'max:255'],
$this->validate([
'firstname' => ['required', 'string', 'max:255'],
'lastname' => ['required', 'string', 'max:255'],
'email' => ['required', 'string', 'lowercase', 'email', 'max:255', 'unique:'.User::class],
'password' => ['required', 'string', 'confirmed', Rules\Password::defaults()],
'workspace' => ['required', 'string', 'max:255'],
'password' => ['required', 'string', Rules\Password::min(10)],
'terms' => ['accepted'],
]);
$validated['password'] = Hash::make($validated['password']);
event(new Registered($user = User::create($validated)));
$user = User::create([
'name' => trim($this->firstname . ' ' . $this->lastname),
'email' => $this->email,
'password' => Hash::make($this->password),
]);
event(new Registered($user));
Auth::login($user);
$this->redirect(route('dashboard', absolute: false), navigate: true);
}
}; ?>
<div>
<h2 style="font-size:1.25rem;font-weight:600;color:#f0f0f5;margin-bottom:1.5rem;">Create account</h2>
<div class="clu-login-grid">
<form wire:submit="register">
<div>
<label for="name" style="display:block;font-size:0.875rem;color:#8888a0;margin-bottom:0.375rem;">Name</label>
<input wire:model="name" id="name" type="text" name="name" required autofocus autocomplete="name"
style="width:100%;padding:0.5rem 0.75rem;background-color:#111117;border:1px solid rgba(255,255,255,0.06);border-radius:0.5rem;color:#f0f0f5;outline:none;"
onfocus="this.style.borderColor='#4f8ef7'" onblur="this.style.borderColor='rgba(255,255,255,0.06)'" />
<x-input-error :messages="$errors->get('name')" class="mt-2" />
{{-- ===== LEFT: FORM ===== --}}
<section class="clu-form-side flex flex-col min-h-screen">
{{-- Top row --}}
<div class="flex items-center justify-between gap-4 flex-wrap gap-y-2">
<div class="flex items-center gap-[10px] shrink-0">
<div class="clu-brand-mark w-[30px] h-[30px] rounded-[8px]" aria-hidden="true"></div>
<div class="font-semibold text-[15px] tracking-[-0.01em]">
CluPilot<span class="text-t2 font-medium ml-[5px] text-[11.5px]">v2.4</span>
</div>
</div>
<div class="clu-login-status text-[12.5px] text-t2 shrink-0">
Schon Konto?
<a href="{{ route('login') }}" wire:navigate class="ml-[6px] font-medium text-akzent hover:underline">Anmelden </a>
</div>
</div>
<div style="margin-top:1rem;">
<label for="email" style="display:block;font-size:0.875rem;color:#8888a0;margin-bottom:0.375rem;">Email</label>
<input wire:model="email" id="email" type="email" name="email" required autocomplete="username"
style="width:100%;padding:0.5rem 0.75rem;background-color:#111117;border:1px solid rgba(255,255,255,0.06);border-radius:0.5rem;color:#f0f0f5;outline:none;"
onfocus="this.style.borderColor='#4f8ef7'" onblur="this.style.borderColor='rgba(255,255,255,0.06)'" />
<x-input-error :messages="$errors->get('email')" class="mt-2" />
{{-- Stepper --}}
<nav class="clu-stepper" aria-label="Registrierungs-Schritte">
<div class="clu-step active">
<div class="clu-step-num" aria-current="step">1</div>
<div class="clu-step-label">Konto</div>
</div>
<div style="margin-top:1rem;">
<label for="password" style="display:block;font-size:0.875rem;color:#8888a0;margin-bottom:0.375rem;">Password</label>
<input wire:model="password" id="password" type="password" name="password" required autocomplete="new-password"
style="width:100%;padding:0.5rem 0.75rem;background-color:#111117;border:1px solid rgba(255,255,255,0.06);border-radius:0.5rem;color:#f0f0f5;outline:none;"
onfocus="this.style.borderColor='#4f8ef7'" onblur="this.style.borderColor='rgba(255,255,255,0.06)'" />
<x-input-error :messages="$errors->get('password')" class="mt-2" />
<div class="clu-step-line" role="separator"></div>
<div class="clu-step">
<div class="clu-step-num">2</div>
<div class="clu-step-label">Workspace</div>
</div>
<div style="margin-top:1rem;">
<label for="password_confirmation" style="display:block;font-size:0.875rem;color:#8888a0;margin-bottom:0.375rem;">Confirm Password</label>
<input wire:model="password_confirmation" id="password_confirmation" type="password" name="password_confirmation" required autocomplete="new-password"
style="width:100%;padding:0.5rem 0.75rem;background-color:#111117;border:1px solid rgba(255,255,255,0.06);border-radius:0.5rem;color:#f0f0f5;outline:none;"
onfocus="this.style.borderColor='#4f8ef7'" onblur="this.style.borderColor='rgba(255,255,255,0.06)'" />
<x-input-error :messages="$errors->get('password_confirmation')" class="mt-2" />
<div class="clu-step-line" role="separator"></div>
<div class="clu-step">
<div class="clu-step-num">3</div>
<div class="clu-step-label">Erster Server</div>
</div>
</nav>
<div style="display:flex;align-items:center;justify-content:space-between;margin-top:1.5rem;">
<a href="{{ route('login') }}" wire:navigate
style="font-size:0.875rem;color:#4f8ef7;text-decoration:none;"
onmouseover="this.style.opacity='.8'" onmouseout="this.style.opacity='1'">
Already have an account?
</a>
{{-- Form wrap --}}
<div class="my-auto flex flex-col gap-[22px] w-full py-6 sm:py-10 mx-auto" style="max-width:420px;">
<button type="submit"
style="padding:0.5rem 1.5rem;background-color:#4f8ef7;color:#fff;border:none;border-radius:0.5rem;font-weight:500;cursor:pointer;"
onmouseover="this.style.opacity='.9'" onmouseout="this.style.opacity='1'">
Register
<header>
<h1 class="m-0 text-[24px] font-semibold tracking-[-0.02em] leading-[1.2]">14 Tage kostenlos testen</h1>
<p class="mt-[6px] text-[13.5px] text-t2">Keine Kreditkarte. Volle Funktionen. Jederzeit kündbar.</p>
</header>
{{-- SSO --}}
<div class="clu-sso-row">
<button class="clu-sso-btn" type="button">
<svg width="15" height="15" viewBox="0 0 24 24" aria-hidden="true">
<path fill="#4285F4" d="M22.5 12.27c0-.79-.07-1.54-.2-2.27H12v4.51h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.32z"/>
<path fill="#34A853" d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.99.66-2.25 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84A11 11 0 0 0 12 23z"/>
<path fill="#FBBC05" d="M5.84 14.10A6.6 6.6 0 0 1 5.5 12c0-.73.13-1.44.34-2.10V7.06H2.18A11 11 0 0 0 1 12c0 1.77.42 3.45 1.18 4.94l3.66-2.84z"/>
<path fill="#EA4335" d="M12 5.38c1.62 0 3.07.56 4.21 1.64l3.15-3.15C17.45 2.10 14.96 1 12 1A11 11 0 0 0 2.18 7.06l3.66 2.84C6.71 7.30 9.14 5.38 12 5.38z"/>
</svg>
Mit Google fortfahren
</button>
<button class="clu-sso-btn" type="button">
<svg width="15" height="15" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
<path d="M12 .5C5.65.5.5 5.65.5 12c0 5.08 3.29 9.39 7.86 10.91.58.11.79-.25.79-.55v-2.02c-3.2.7-3.87-1.36-3.87-1.36-.52-1.33-1.27-1.68-1.27-1.68-1.04-.71.08-.7.08-.7 1.15.08 1.76 1.18 1.76 1.18 1.02 1.75 2.69 1.24 3.35.95.1-.74.4-1.24.73-1.53-2.55-.29-5.24-1.28-5.24-5.69 0-1.26.45-2.28 1.18-3.09-.12-.29-.51-1.46.11-3.05 0 0 .97-.31 3.18 1.18.92-.26 1.91-.39 2.89-.39.98 0 1.97.13 2.89.39 2.20-1.49 3.17-1.18 3.17-1.18.63 1.59.23 2.76.12 3.05.74.81 1.18 1.83 1.18 3.09 0 4.42-2.69 5.40-5.25 5.68.41.36.78 1.06.78 2.14v3.18c0 .31.21.67.79.55C20.21 21.39 23.5 17.08 23.5 12c0-6.35-5.15-11.5-11.5-11.5z"/>
</svg>
Mit GitHub fortfahren
</button>
</div>
<div class="clu-divider">oder mit E-Mail</div>
{{-- Form --}}
<form wire:submit="register" class="flex flex-col gap-[14px]">
{{-- First / Last name --}}
<div class="clu-field-row">
<div class="flex flex-col gap-[6px]">
<label for="firstname" class="text-[11.5px] text-t2 font-medium">Vorname</label>
<div class="clu-input">
<input wire:model="firstname" id="firstname" name="firstname" type="text"
placeholder="Marie" autocomplete="given-name" required autofocus />
</div>
<x-input-error :messages="$errors->get('firstname')" class="mt-1" />
</div>
<div class="flex flex-col gap-[6px]">
<label for="lastname" class="text-[11.5px] text-t2 font-medium">Nachname</label>
<div class="clu-input">
<input wire:model="lastname" id="lastname" name="lastname" type="text"
placeholder="Weber" autocomplete="family-name" required />
</div>
<x-input-error :messages="$errors->get('lastname')" class="mt-1" />
</div>
</div>
{{-- Email --}}
<div class="flex flex-col gap-[6px]">
<label for="reg-email" class="text-[11.5px] text-t2 font-medium">Arbeits-E-Mail</label>
<div class="clu-input" id="reg-email-wrap">
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="text-dim flex-none" aria-hidden="true">
<rect x="3" y="5" width="18" height="14" rx="2"/><path d="M3 7l9 6 9-6"/>
</svg>
<input wire:model="email" id="reg-email" name="email" type="email"
placeholder="du@firma.de" autocomplete="email" required />
<svg class="clu-input-check" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" aria-hidden="true">
<path d="M5 12l5 5L20 7"/>
</svg>
</div>
<x-input-error :messages="$errors->get('email')" class="mt-1" />
</div>
{{-- Workspace --}}
<div class="flex flex-col gap-[6px]">
<label for="reg-workspace" class="flex items-center justify-between text-[11.5px] text-t2 font-medium">
Workspace-Name
<span class="font-normal font-mono text-[10.5px] text-dim">URL für dein Team</span>
</label>
<div class="clu-input with-suffix">
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="text-dim flex-none" aria-hidden="true">
<rect x="3" y="4" width="18" height="7" rx="1.5"/><rect x="3" y="13" width="18" height="7" rx="1.5"/>
<circle cx="7" cy="7.5" r="0.7" fill="currentColor"/><circle cx="7" cy="16.5" r="0.7" fill="currentColor"/>
</svg>
<input wire:model="workspace" id="reg-workspace" name="workspace" type="text"
placeholder="Acme Cluster" required />
<span class="clu-input-suffix">.clupilot.io</span>
</div>
<div class="clu-slug-preview">Erreichbar unter <b id="reg-slug-preview">dein-team</b>.clupilot.io</div>
<x-input-error :messages="$errors->get('workspace')" class="mt-1" />
</div>
{{-- Password --}}
<div class="flex flex-col gap-[6px]">
<label for="reg-password" class="flex items-center justify-between text-[11.5px] text-t2 font-medium">
Passwort
<span class="font-normal font-mono text-[10.5px] text-dim">min. 10 Zeichen</span>
</label>
<div class="clu-input">
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="text-dim flex-none" aria-hidden="true">
<rect x="5" y="11" width="14" height="9" rx="2"/><path d="M8 11V7a4 4 0 0 1 8 0v4"/>
</svg>
<input wire:model="password" id="reg-password" name="password" type="password"
placeholder="••••••••••••" autocomplete="new-password" required />
<button type="button" id="reg-toggle-eye" aria-label="Passwort anzeigen"
class="text-t2 p-[4px] rounded-[4px] grid place-items-center transition-colors hover:text-t1 hover:bg-surface2">
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M1 12s4-7 11-7 11 7 11 7-4 7-11 7S1 12 1 12z"/><circle cx="12" cy="12" r="3"/>
</svg>
</button>
</div>
<div class="clu-pw-meter" id="reg-pw-meter"><span></span><span></span><span></span><span></span></div>
<div class="clu-pw-rules">
<span class="clu-pw-rule" data-rule="len"><span class="clu-pw-bullet"></span>10+ Zeichen</span>
<span class="clu-pw-rule" data-rule="upper"><span class="clu-pw-bullet"></span>Großbuchstabe</span>
<span class="clu-pw-rule" data-rule="num"><span class="clu-pw-bullet"></span>Zahl</span>
<span class="clu-pw-rule" data-rule="sym"><span class="clu-pw-bullet"></span>Sonderzeichen</span>
</div>
<x-input-error :messages="$errors->get('password')" class="mt-1" />
</div>
{{-- Terms --}}
<label class="clu-checkbox">
<input wire:model="terms" id="reg-terms" type="checkbox" class="sr-only" />
<span class="clu-check-box">
<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<path d="M5 12l5 5L20 7"/>
</svg>
</span>
<span>Ich akzeptiere die <a href="#" class="text-akzent font-medium hover:underline">AGB</a> und stimme der <a href="#" class="text-akzent font-medium hover:underline">Datenschutzerklärung</a> zu. CluPilot speichert SSH-Keys verschlüsselt in der EU.</span>
</label>
<x-input-error :messages="$errors->get('terms')" class="mt-[-6px]" />
{{-- Submit --}}
<button class="clu-btn-primary" type="submit" wire:loading.attr="disabled">
<svg wire:loading class="clu-spin" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" aria-hidden="true">
<path d="M21 12a9 9 0 1 1-6.2-8.55"/>
</svg>
<span wire:loading.remove>Konto erstellen</span>
<span wire:loading>Konto wird angelegt…</span>
<svg class="arrow" wire:loading.remove width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" aria-hidden="true">
<path d="M5 12h14M13 5l7 7-7 7"/>
</svg>
</button>
</form>
</div>
{{-- Footer --}}
<footer class="flex items-center justify-between gap-3 text-t2 text-[11.5px] font-mono pt-3 border-t border-white/6 flex-wrap">
<div class="flex items-center gap-[6px]">
<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" aria-hidden="true">
<rect x="5" y="11" width="14" height="9" rx="2"/><path d="M8 11V7a4 4 0 0 1 8 0v4"/>
</svg>
TLS 1.3 · AES-256 · region eu-central-1
</div>
<div class="flex gap-[14px]">
<a href="#" class="text-t2 hover:text-t1 no-underline transition-colors">Datenschutz</a>
<a href="#" class="text-t2 hover:text-t1 no-underline transition-colors">AGB</a>
<a href="#" class="text-t2 hover:text-t1 no-underline transition-colors">Status</a>
<a href="#" class="text-t2 hover:text-t1 no-underline transition-colors">Hilfe</a>
</div>
</footer>
</section>
{{-- ===== RIGHT: BENEFITS PANEL ===== --}}
<aside class="clu-live-side relative border-l border-white/6 overflow-hidden flex flex-col gap-[22px] min-h-screen"
style="background:linear-gradient(180deg,#0c0c12 0%,#0a0a0f 100%);padding:36px 40px;"
aria-hidden="true">
<div class="clu-trial-card">
<div class="clu-trial-row">
<span class="clu-trial-tag">14 Tage Pro · gratis</span>
<span class="clu-trial-count">
<span class="clu-pulse-dot w-[6px] h-[6px] rounded-full bg-success flex-none"></span>
47 Teams diese Woche
</span>
</div>
<div class="clu-trial-title">In <em>60 Sekunden</em> deinen ersten Server verbunden.</div>
<div class="clu-trial-sub">SSH-Schlüssel hochladen, Site importieren, fertig. Wir kümmern uns ums Monitoring, Backups und Updates du um dein Business.</div>
</div>
<ul class="clu-benefit-list">
<li class="clu-benefit">
<div class="clu-benefit-ico" aria-hidden="true">
<svg 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>
</div>
<div>
<div class="clu-benefit-title">SSH-First Setup</div>
<div class="clu-benefit-desc">Verbinde Hetzner, Hostinger oder deinen eigenen Server in 2 Klicks keine Agents, kein Custom-DNS.</div>
</div>
</li>
<li class="clu-benefit">
<div class="clu-benefit-ico success" aria-hidden="true">
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M21 12c0 5-9 9-9 9s-9-4-9-9V5l9-3 9 3v7z"/><path d="M9 11l2 2 4-4"/>
</svg>
</div>
<div>
<div class="clu-benefit-title">Stündliche Backups inklusive</div>
<div class="clu-benefit-desc">Verschlüsselt nach Frankfurt-S3, 30 Tage Retention, One-Click-Restore auch für die WP-Datenbank.</div>
</div>
</li>
<li class="clu-benefit">
<div class="clu-benefit-ico purple" aria-hidden="true">
<svg width="15" height="15" 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>
</div>
<div>
<div class="clu-benefit-title">Bulk-Updates ohne Drama</div>
<div class="clu-benefit-desc">WordPress, Plugins und Themes auf 100 Sites gleichzeitig mit Staging-Diff und automatischem Rollback bei Fehler.</div>
</div>
</li>
<li class="clu-benefit">
<div class="clu-benefit-ico warn" aria-hidden="true">
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M12 3l8 4v5c0 5-3.5 8-8 9-4.5-1-8-4-8-9V7l8-4z"/><path d="M12 8v5M12 16h.01"/>
</svg>
</div>
<div>
<div class="clu-benefit-title">Brute-Force-Defense aktiv</div>
<div class="clu-benefit-desc">Geo-Fencing, Rate-Limits und WP-User-Lockout schon im Trial dabei. Letzte 24h: 12.488 Versuche blockiert.</div>
</div>
</li>
</ul>
<div class="clu-quote-card">
<div class="clu-quote-text">Statt 18 SSH-Sessions in Terminal-Tabs habe ich jetzt eine Konsole. Mein Agency-Team hostet 87 Sites und schläft endlich wieder durch.</div>
<div class="clu-quote-author">
<div class="clu-avatar">JK</div>
<div>
<div class="clu-author-name">Julian Koch</div>
<div class="clu-author-role">CTO · Helio Digital · 87 Sites</div>
</div>
</div>
</div>
<div class="clu-logos">
<span>vertrauen uns</span>
<span class="clu-logo-pill">helio.</span>
<span class="clu-logo-pill">acme▲</span>
<span class="clu-logo-pill">opencrate</span>
<span class="clu-logo-pill">bergmann</span>
<span class="clu-logo-pill">+ 2.1k</span>
</div>
</aside>
</div>

View File

@ -1,6 +1,8 @@
<?php
use App\Http\Controllers\Auth\VerifyEmailController;
use App\Livewire\Actions\Logout;
use Illuminate\Auth\Events\Verified;
use Illuminate\Foundation\Auth\EmailVerificationRequest;
use Illuminate\Support\Facades\Route;
use Livewire\Volt\Volt;
@ -22,10 +24,21 @@ Route::middleware('auth')->group(function () {
Volt::route('verify-email', 'pages.auth.verify-email')
->name('verification.notice');
Route::get('verify-email/{id}/{hash}', VerifyEmailController::class)
->middleware(['signed', 'throttle:6,1'])
->name('verification.verify');
Route::get('verify-email/{id}/{hash}', function (EmailVerificationRequest $request) {
if ($request->user()->hasVerifiedEmail()) {
return redirect()->intended(route('dashboard', absolute: false).'?verified=1');
}
if ($request->user()->markEmailAsVerified()) {
event(new Verified($request->user()));
}
return redirect()->intended(route('dashboard', absolute: false).'?verified=1');
})->middleware(['signed', 'throttle:6,1'])->name('verification.verify');
Volt::route('confirm-password', 'pages.auth.confirm-password')
->name('password.confirm');
Route::post('logout', function (Logout $logout) {
$logout();
return redirect()->route('login');
})->name('logout');
});

4
app/storage/app/.gitignore vendored Executable file
View File

@ -0,0 +1,4 @@
*
!private/
!public/
!.gitignore

2
app/storage/app/private/.gitignore vendored Executable file
View File

@ -0,0 +1,2 @@
*
!.gitignore

2
app/storage/app/public/.gitignore vendored Executable file
View File

@ -0,0 +1,2 @@
*
!.gitignore

9
app/storage/framework/.gitignore vendored Executable file
View File

@ -0,0 +1,9 @@
compiled.php
config.php
down
events.scanned.php
maintenance.php
routes.php
routes.scanned.php
schedule-*
services.json

3
app/storage/framework/cache/.gitignore vendored Executable file
View File

@ -0,0 +1,3 @@
*
!data/
!.gitignore

2
app/storage/framework/cache/data/.gitignore vendored Executable file
View File

@ -0,0 +1,2 @@
*
!.gitignore

2
app/storage/framework/sessions/.gitignore vendored Executable file
View File

@ -0,0 +1,2 @@
*
!.gitignore

2
app/storage/framework/testing/.gitignore vendored Executable file
View File

@ -0,0 +1,2 @@
*
!.gitignore

2
app/storage/framework/views/.gitignore vendored Executable file
View File

@ -0,0 +1,2 @@
*
!.gitignore

2
app/storage/logs/.gitignore vendored Executable file
View File

@ -0,0 +1,2 @@
*
!.gitignore

View File

@ -1,76 +1,118 @@
name: ${STACK_NAME:-clupilot}
services:
nginx:
image: nginx:1.27-alpine
container_name: ${STACK_NAME:-clupilot}-nginx
restart: unless-stopped
ports:
- "80:80"
- "${HTTP_PORT:-80}:80"
volumes:
- ./app:/var/www/html
- ./docker/nginx/default.conf:/etc/nginx/conf.d/default.conf:ro
extra_hosts:
- "host.docker.internal:host-gateway"
depends_on: [app, reverb]
depends_on:
app:
condition: service_started
reverb:
condition: service_started
networks: [clu]
healthcheck:
test: ["CMD", "wget", "-qO-", "http://localhost/"]
interval: 30s
timeout: 5s
retries: 3
app:
build: ./docker/php
image: ${STACK_NAME:-clupilot}/php:latest
container_name: ${STACK_NAME:-clupilot}-app
restart: unless-stopped
volumes:
- ./app:/var/www/html
depends_on: [mysql, redis]
depends_on:
mysql:
condition: service_healthy
redis:
condition: service_healthy
networks: [clu]
queue:
build: ./docker/php
image: ${STACK_NAME:-clupilot}/php:latest
container_name: ${STACK_NAME:-clupilot}-queue
restart: unless-stopped
command: php artisan queue:work --tries=3 --timeout=120
volumes:
- ./app:/var/www/html
depends_on: [mysql, redis]
depends_on:
app:
condition: service_started
networks: [clu]
scheduler:
build: ./docker/php
image: ${STACK_NAME:-clupilot}/php:latest
container_name: ${STACK_NAME:-clupilot}-scheduler
restart: unless-stopped
command: php artisan schedule:work
volumes:
- ./app:/var/www/html
depends_on: [mysql, redis]
depends_on:
app:
condition: service_started
networks: [clu]
reverb:
build: ./docker/php
image: ${STACK_NAME:-clupilot}/php:latest
container_name: ${STACK_NAME:-clupilot}-reverb
restart: unless-stopped
command: php artisan reverb:start --host=0.0.0.0 --port=8080
volumes:
- ./app:/var/www/html
depends_on: [redis]
depends_on:
app:
condition: service_started
networks: [clu]
mysql:
image: mariadb:11
container_name: ${STACK_NAME:-clupilot}-mysql
restart: unless-stopped
environment:
MARIADB_DATABASE: clupilot
MARIADB_USER: clupilot
MARIADB_PASSWORD: clupilot
MARIADB_ROOT_PASSWORD: rootpw
MARIADB_DATABASE: ${DB_DATABASE:-clupilot}
MARIADB_USER: ${DB_USERNAME:-clupilot}
MARIADB_PASSWORD: ${DB_PASSWORD:?DB_PASSWORD required}
MARIADB_ROOT_PASSWORD: ${DB_ROOT_PASSWORD:?DB_ROOT_PASSWORD required}
volumes:
- mysql-data:/var/lib/mysql
networks: [clu]
healthcheck:
test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"]
interval: 10s
timeout: 5s
retries: 10
start_period: 30s
redis:
image: redis:7-alpine
container_name: ${STACK_NAME:-clupilot}-redis
restart: unless-stopped
volumes:
- redis-data:/data
networks: [clu]
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 3s
retries: 5
mailpit:
image: axllent/mailpit:latest
container_name: ${STACK_NAME:-clupilot}-mailpit
restart: unless-stopped
profiles: ["dev"]
ports:
- "8025:8025"
- "${MAILPIT_UI_PORT:-8025}:8025"
networks: [clu]
volumes:

View File

@ -15,17 +15,7 @@ server {
index index.php;
client_max_body_size 100M;
# Vite Dev-Assets über gleiche Domain
location ^~ /build/ {
proxy_pass http://vite;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Proto https;
}
# Vite HMR WebSocket
# Vite HMR WebSocket — only location that needs Connection: upgrade
location /vite-hmr {
proxy_pass http://vite;
proxy_http_version 1.1;
@ -35,13 +25,35 @@ server {
proxy_read_timeout 86400;
}
# Vite Module-Resolution
# Production build assets — serve statically (never proxied to Vite)
location ^~ /build/ {
alias /var/www/html/public/build/;
try_files $uri =404;
expires 1y;
add_header Cache-Control "public, immutable";
}
# CSS files — force Accept: text/css so Vite returns actual CSS (not JS module wrapper)
location ~ \.css(\?.*)?$ {
proxy_pass http://vite;
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_set_header Host $host;
proxy_set_header Accept "text/css";
proxy_hide_header Cache-Control;
proxy_hide_header Expires;
add_header Cache-Control "no-store, no-cache, must-revalidate" always;
}
# Vite module resolution (@vite, @id, @fs, resources, node_modules)
location ~ ^/(@vite|@id|@fs|resources|node_modules)/ {
proxy_pass http://vite;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Connection "";
proxy_set_header Host $host;
proxy_hide_header Cache-Control;
proxy_hide_header Expires;
add_header Cache-Control "no-store, no-cache, must-revalidate" always;
}
location / {

14
stack.env.example Normal file
View File

@ -0,0 +1,14 @@
# Portainer Stack environment variables.
# In Portainer: Stack → Environment variables → paste these (set real secrets).
STACK_NAME=clupilot
# Host port mappings
HTTP_PORT=80
MAILPIT_UI_PORT=8025
# Database (MariaDB)
DB_DATABASE=clupilot
DB_USERNAME=clupilot
DB_PASSWORD=change-me
DB_ROOT_PASSWORD=change-me-root

View File

@ -0,0 +1,911 @@
<!doctype html>
<html lang="de">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>CluPilot — Dashboard</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet">
<style>
:root{
--bg:#0a0a0f;
--card:#16161e;
--card-hover:#1a1a24;
--border:rgba(255,255,255,0.06);
--border-strong:rgba(255,255,255,0.10);
--fg:#f0f0f5;
--muted:#8888a0;
--muted-2:#5c5c70;
--accent:#4f8ef7;
--accent-soft:rgba(79,142,247,0.12);
--success:#3ecf8e;
--warning:#f5a623;
--danger:#f05252;
--success-soft:rgba(62,207,142,0.12);
--warning-soft:rgba(245,166,35,0.12);
--danger-soft:rgba(240,82,82,0.12);
--radius:10px;
--radius-sm:6px;
--mono:'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, monospace;
}
*{box-sizing:border-box}
html,body{margin:0;padding:0;background:var(--bg);color:var(--fg);font-family:'Inter',system-ui,-apple-system,sans-serif;font-size:13px;line-height:1.45;-webkit-font-smoothing:antialiased}
a{color:inherit;text-decoration:none}
button{font-family:inherit;color:inherit;background:none;border:0;cursor:pointer}
/* ---------- App layout ---------- */
.app{display:grid;grid-template-columns:220px 1fr;min-height:100vh}
/* ---------- Sidebar ---------- */
.sidebar{position:sticky;top:0;height:100vh;background:#0c0c12;border-right:1px solid var(--border);display:flex;flex-direction:column;padding:16px 12px;gap:14px}
.brand{display:flex;align-items:center;gap:9px;padding:4px 6px 8px;border-bottom:1px solid var(--border)}
.brand-mark{width:26px;height:26px;border-radius:7px;background:linear-gradient(135deg,#4f8ef7 0%,#7c5cff 100%);position:relative;display:grid;place-items:center;box-shadow:0 0 0 1px rgba(255,255,255,0.06) inset, 0 4px 12px rgba(79,142,247,0.25)}
.brand-mark::before{content:"";width:10px;height:10px;border-radius:50%;background:#0c0c12;box-shadow:inset 0 0 0 2px #fff}
.brand-name{font-weight:600;font-size:14px;letter-spacing:-0.01em}
.brand-name span{color:var(--muted);font-weight:500;margin-left:4px;font-size:11px}
.ws-switch{display:flex;align-items:center;gap:9px;padding:8px 9px;border:1px solid var(--border);border-radius:var(--radius-sm);background:#101019;cursor:pointer;transition:background .15s}
.ws-switch:hover{background:#14141e}
.ws-avatar{width:22px;height:22px;border-radius:5px;background:linear-gradient(135deg,#3ecf8e,#2a9d6f);display:grid;place-items:center;color:#0a0a0f;font-weight:700;font-size:11px;flex:none}
.ws-meta{flex:1;min-width:0}
.ws-name{font-size:12.5px;font-weight:500;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.ws-plan{font-size:10.5px;color:var(--muted);font-family:var(--mono)}
.ws-chev{color:var(--muted-2)}
.nav{display:flex;flex-direction:column;gap:14px;flex:1;overflow-y:auto;margin:0 -4px;padding:0 4px}
.nav-section{display:flex;flex-direction:column;gap:1px}
.nav-title{font-size:10.5px;text-transform:uppercase;letter-spacing:0.08em;color:var(--muted-2);padding:6px 9px 4px;font-weight:600}
.nav-item{display:flex;align-items:center;gap:9px;padding:6.5px 9px;border-radius:6px;color:#c8c8d8;cursor:pointer;font-size:12.5px;transition:background .12s,color .12s;position:relative}
.nav-item:hover{background:#14141e;color:var(--fg)}
.nav-item.active{background:#181822;color:var(--fg)}
.nav-item.active::before{content:"";position:absolute;left:-12px;top:6px;bottom:6px;width:2px;border-radius:2px;background:var(--accent)}
.nav-item .ico{width:15px;height:15px;color:var(--muted);flex:none}
.nav-item.active .ico{color:var(--accent)}
.nav-item .count{margin-left:auto;font-size:10.5px;font-family:var(--mono);color:var(--muted);background:#14141e;padding:1px 6px;border-radius:10px;border:1px solid var(--border)}
.nav-item.active .count{color:var(--fg);background:#1f1f2c}
.plan-box{margin-top:auto;padding:12px;border:1px solid var(--border);border-radius:var(--radius);background:linear-gradient(180deg,#11111a 0%,#0e0e15 100%);position:relative;overflow:hidden}
.plan-box::before{content:"";position:absolute;inset:0;background:radial-gradient(120% 80% at 0% 0%,rgba(79,142,247,0.10),transparent 60%);pointer-events:none}
.plan-row{display:flex;align-items:center;justify-content:space-between;font-size:11.5px;position:relative}
.plan-name{font-weight:600}
.plan-tag{font-family:var(--mono);font-size:10px;color:var(--accent);background:var(--accent-soft);padding:2px 6px;border-radius:4px;border:1px solid rgba(79,142,247,0.25)}
.plan-meter{margin-top:9px;height:5px;border-radius:3px;background:#191923;overflow:hidden;position:relative}
.plan-meter span{display:block;height:100%;width:62%;background:linear-gradient(90deg,#4f8ef7,#7c5cff);border-radius:3px}
.plan-stats{display:flex;justify-content:space-between;margin-top:6px;font-size:10.5px;color:var(--muted);font-family:var(--mono)}
.plan-cta{margin-top:10px;width:100%;padding:7px;border:1px solid var(--border-strong);border-radius:6px;font-size:11.5px;font-weight:500;color:var(--fg);background:#16161e;transition:background .15s}
.plan-cta:hover{background:#1d1d28}
/* ---------- Main ---------- */
.main{display:flex;flex-direction:column;min-width:0}
/* Topbar */
.topbar{position:sticky;top:0;z-index:5;display:flex;align-items:center;gap:14px;padding:14px 22px;background:rgba(10,10,15,0.85);backdrop-filter:blur(12px);border-bottom:1px solid var(--border)}
.crumbs{display:flex;align-items:center;gap:8px;min-width:0}
.crumb{color:var(--muted);font-size:12.5px}
.crumb.sep{color:var(--muted-2)}
.page-title{font-size:14.5px;font-weight:600;letter-spacing:-0.01em}
.status-chip{display:inline-flex;align-items:center;gap:6px;padding:3px 8px 3px 7px;border:1px solid rgba(62,207,142,0.25);background:var(--success-soft);color:var(--success);border-radius:20px;font-size:11px;font-weight:500;font-family:var(--mono)}
.pulse-dot{width:6px;height:6px;border-radius:50%;background:currentColor;position:relative;flex:none}
.pulse-dot::after{content:"";position:absolute;inset:-3px;border-radius:50%;background:currentColor;opacity:0.35;animation:pulse 1.8s ease-out infinite}
@keyframes pulse{0%{transform:scale(0.6);opacity:.5}100%{transform:scale(2.2);opacity:0}}
.top-spacer{flex:1}
.search{display:flex;align-items:center;gap:8px;padding:6px 10px;background:#101019;border:1px solid var(--border);border-radius:7px;min-width:280px;color:var(--muted);transition:border-color .15s}
.search:hover{border-color:var(--border-strong)}
.search:focus-within{border-color:rgba(79,142,247,0.5);background:#12121c}
.search input{flex:1;background:none;border:0;outline:0;color:var(--fg);font-size:12.5px;font-family:inherit;min-width:0}
.search input::placeholder{color:var(--muted)}
.kbd{font-family:var(--mono);font-size:10.5px;color:var(--muted);padding:1px 5px;border:1px solid var(--border);border-radius:4px;background:#0c0c14}
.icon-btn{width:32px;height:32px;border:1px solid var(--border);border-radius:7px;display:grid;place-items:center;color:var(--muted);background:#101019;transition:all .15s;position:relative}
.icon-btn:hover{color:var(--fg);border-color:var(--border-strong);background:#15151f}
.icon-btn .badge{position:absolute;top:-3px;right:-3px;background:var(--danger);color:#fff;font-size:9.5px;font-weight:600;min-width:16px;height:16px;border-radius:8px;display:grid;place-items:center;padding:0 4px;border:2px solid var(--bg);font-family:var(--mono)}
.btn-primary{display:inline-flex;align-items:center;gap:6px;padding:7px 12px;background:var(--accent);color:#fff;border-radius:7px;font-weight:500;font-size:12.5px;transition:background .15s;box-shadow:0 1px 0 rgba(255,255,255,0.08) inset, 0 1px 3px rgba(79,142,247,0.35)}
.btn-primary:hover{background:#5e98f7}
/* Content */
.content{padding:22px;display:flex;flex-direction:column;gap:22px;max-width:1600px;width:100%;margin:0 auto}
.section-head{display:flex;align-items:center;justify-content:space-between;margin-bottom:-6px}
.section-title{font-size:13.5px;font-weight:600;letter-spacing:-0.005em;display:flex;align-items:center;gap:9px}
.section-title .live{font-size:10.5px;color:var(--success);font-family:var(--mono);display:inline-flex;align-items:center;gap:5px;font-weight:500}
.section-sub{font-size:12px;color:var(--muted);margin-top:2px}
.section-actions{display:flex;gap:8px;align-items:center}
.ghost-btn{padding:5px 9px;font-size:11.5px;color:var(--muted);border:1px solid var(--border);border-radius:6px;background:#101019;transition:all .15s}
.ghost-btn:hover{color:var(--fg);border-color:var(--border-strong)}
.ghost-btn.active{color:var(--fg);background:#1a1a24;border-color:var(--border-strong)}
/* ---------- Metric cards ---------- */
.metrics{display:grid;grid-template-columns:repeat(6,1fr);gap:12px}
.metric{position:relative;background:var(--card);border:1px solid var(--border);border-radius:var(--radius);padding:14px 14px 13px;overflow:hidden;transition:border-color .15s,transform .15s}
.metric:hover{border-color:var(--border-strong)}
.metric::before{content:"";position:absolute;top:0;left:0;right:0;height:2px;background:var(--accent)}
.metric.success::before{background:var(--success)}
.metric.warning::before{background:var(--warning)}
.metric.danger::before{background:var(--danger)}
.metric.neutral::before{background:var(--muted-2)}
.metric-label{display:flex;align-items:center;gap:7px;color:var(--muted);font-size:11.5px;font-weight:500}
.metric-label .ico{width:13px;height:13px}
.metric-value{margin-top:8px;font-family:var(--mono);font-size:22px;font-weight:600;letter-spacing:-0.02em;color:var(--fg);line-height:1.1}
.metric-value .sub{font-size:13px;color:var(--muted);font-weight:500;margin-left:2px}
.metric-delta{margin-top:6px;font-size:11px;display:flex;align-items:center;gap:6px;color:var(--muted)}
.metric-delta .chg{font-family:var(--mono);font-weight:500}
.metric-delta .chg.up{color:var(--success)}
.metric-delta .chg.down{color:var(--danger)}
.metric-delta .chg.warn{color:var(--warning)}
.metric .spark{position:absolute;right:10px;bottom:10px;opacity:0.7}
/* ---------- Card primitive ---------- */
.card{background:var(--card);border:1px solid var(--border);border-radius:var(--radius);overflow:hidden}
.card-head{display:flex;align-items:center;justify-content:space-between;padding:13px 16px 12px;border-bottom:1px solid var(--border)}
.card-head h3{margin:0;font-size:13px;font-weight:600;letter-spacing:-0.005em;display:flex;align-items:center;gap:8px}
.card-head .meta{font-size:11.5px;color:var(--muted);font-family:var(--mono)}
.card-tabs{display:flex;gap:2px}
.card-tabs button{padding:4px 9px;font-size:11.5px;color:var(--muted);border-radius:5px;transition:all .15s}
.card-tabs button:hover{color:var(--fg)}
.card-tabs button.active{color:var(--fg);background:#1d1d28}
/* ---------- Tables ---------- */
.table{width:100%;border-collapse:collapse;font-size:12.5px}
.table th{text-align:left;font-weight:500;color:var(--muted);font-size:11px;letter-spacing:0.04em;text-transform:uppercase;padding:9px 14px;border-bottom:1px solid var(--border);background:#101019;position:sticky;top:0}
.table th.num{text-align:right}
.table td{padding:11px 14px;border-bottom:1px solid var(--border);vertical-align:middle}
.table tbody tr{transition:background .12s}
.table tbody tr:hover{background:#1a1a24}
.table tbody tr:last-child td{border-bottom:0}
.table td.num{text-align:right;font-family:var(--mono)}
.table td.mono{font-family:var(--mono);color:var(--muted);font-size:11.5px}
/* Server cells */
.server-cell{display:flex;align-items:center;gap:10px}
.server-icon{width:30px;height:30px;border-radius:7px;background:#1d1d28;border:1px solid var(--border);display:grid;place-items:center;color:var(--muted);flex:none}
.server-icon.up{color:var(--success);background:rgba(62,207,142,0.06);border-color:rgba(62,207,142,0.18)}
.server-icon.warn{color:var(--warning);background:rgba(245,166,35,0.06);border-color:rgba(245,166,35,0.18)}
.server-meta{display:flex;flex-direction:column;gap:1px;min-width:0}
.server-name{font-weight:500;color:var(--fg);font-size:12.5px}
.server-ip{font-family:var(--mono);font-size:10.5px;color:var(--muted)}
/* Resource bars */
.res{display:flex;flex-direction:column;gap:3px;min-width:90px}
.res-top{display:flex;align-items:center;justify-content:space-between;font-size:10.5px;color:var(--muted);font-family:var(--mono)}
.res-top .val{color:var(--fg)}
.res-bar{height:4px;background:#1d1d28;border-radius:2px;overflow:hidden;position:relative}
.res-fill{height:100%;border-radius:2px;background:var(--success);transition:width .3s}
.res-fill.ok{background:var(--success)}
.res-fill.warn{background:var(--warning)}
.res-fill.bad{background:var(--danger)}
/* Resource grid (multi-stat cells) */
.res-grid{display:grid;grid-template-columns:repeat(3,minmax(80px,1fr));gap:10px;min-width:300px}
/* Pills */
.pill{display:inline-flex;align-items:center;gap:5px;padding:2px 7px;font-size:10.5px;font-weight:500;border-radius:4px;font-family:var(--mono);border:1px solid var(--border);background:#1a1a24;color:var(--fg);line-height:1.4}
.pill.muted{color:var(--muted)}
.pill.success{color:var(--success);background:var(--success-soft);border-color:rgba(62,207,142,0.22)}
.pill.warning{color:var(--warning);background:var(--warning-soft);border-color:rgba(245,166,35,0.22)}
.pill.danger{color:var(--danger);background:var(--danger-soft);border-color:rgba(240,82,82,0.22)}
.pill.accent{color:var(--accent);background:var(--accent-soft);border-color:rgba(79,142,247,0.22)}
/* Grade badge */
.grade{display:inline-grid;place-items:center;width:24px;height:24px;border-radius:6px;font-family:var(--mono);font-weight:600;font-size:12px;line-height:1}
.grade.a{background:rgba(62,207,142,0.15);color:var(--success);box-shadow:0 0 0 1px rgba(62,207,142,0.25) inset}
.grade.b{background:rgba(79,142,247,0.15);color:var(--accent);box-shadow:0 0 0 1px rgba(79,142,247,0.25) inset}
.grade.c{background:rgba(245,166,35,0.15);color:var(--warning);box-shadow:0 0 0 1px rgba(245,166,35,0.25) inset}
.grade.d{background:rgba(245,166,35,0.18);color:var(--warning);box-shadow:0 0 0 1px rgba(245,166,35,0.32) inset}
.grade.f{background:rgba(240,82,82,0.15);color:var(--danger);box-shadow:0 0 0 1px rgba(240,82,82,0.28) inset}
/* Site link */
.site-cell{display:flex;align-items:center;gap:10px;min-width:0}
.favicon{width:24px;height:24px;border-radius:5px;display:grid;place-items:center;font-weight:600;font-size:11px;color:#0a0a0f;flex:none;font-family:var(--mono)}
.site-name{font-weight:500;color:var(--fg);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:200px}
.site-meta{font-family:var(--mono);font-size:10.5px;color:var(--muted)}
/* Action icons */
.row-actions{display:flex;gap:4px;justify-content:flex-end;opacity:0;transition:opacity .15s}
tr:hover .row-actions{opacity:1}
.row-actions button{width:26px;height:26px;border-radius:5px;color:var(--muted);display:grid;place-items:center;transition:all .15s}
.row-actions button:hover{color:var(--fg);background:#22222e}
/* ---------- Bottom row ---------- */
.bottom-row{display:grid;grid-template-columns:1.4fr 1fr 1fr;gap:14px}
/* Chart */
.chart-wrap{padding:14px 16px 8px}
.chart-head{display:flex;align-items:flex-end;justify-content:space-between;margin-bottom:6px}
.chart-figure{font-family:var(--mono);font-size:22px;font-weight:600;letter-spacing:-0.02em}
.chart-figure .unit{font-size:13px;color:var(--muted);margin-left:4px;font-weight:500}
.chart-sub{font-size:11px;color:var(--muted);margin-top:2px}
.legend{display:flex;gap:14px;font-size:11px;color:var(--muted);font-family:var(--mono)}
.legend i{display:inline-block;width:8px;height:8px;border-radius:2px;margin-right:5px;vertical-align:middle}
.svg-chart{width:100%;height:160px;display:block}
.chart-axis{font-family:var(--mono);font-size:9.5px;fill:var(--muted-2)}
/* Update pipeline list */
.pipe{display:flex;flex-direction:column}
.pipe-row{display:flex;align-items:center;gap:11px;padding:10px 16px;border-bottom:1px solid var(--border);transition:background .12s}
.pipe-row:last-child{border-bottom:0}
.pipe-row:hover{background:#1a1a24}
.pipe-ico{width:26px;height:26px;border-radius:6px;display:grid;place-items:center;flex:none;border:1px solid var(--border);background:#101019;font-size:10.5px;font-family:var(--mono);font-weight:600;color:var(--muted)}
.pipe-ico.wp{color:#21759b;border-color:rgba(33,117,155,0.3);background:rgba(33,117,155,0.08)}
.pipe-ico.pl{color:var(--accent);border-color:rgba(79,142,247,0.25);background:var(--accent-soft)}
.pipe-ico.php{color:#7c5cff;border-color:rgba(124,92,255,0.28);background:rgba(124,92,255,0.10)}
.pipe-info{flex:1;min-width:0}
.pipe-title{font-size:12.5px;font-weight:500;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.pipe-sub{font-size:11px;color:var(--muted);font-family:var(--mono)}
.pipe-status{font-size:10.5px;font-family:var(--mono)}
/* Mini progress (pipeline) */
.mini-bar{width:60px;height:4px;background:#1d1d28;border-radius:2px;overflow:hidden}
.mini-bar span{display:block;height:100%;background:var(--accent);border-radius:2px}
/* Activity feed */
.feed{padding:6px 0}
.feed-row{display:flex;gap:11px;padding:10px 16px;align-items:flex-start;position:relative;transition:background .12s}
.feed-row:hover{background:#1a1a24}
.feed-ico{width:24px;height:24px;border-radius:6px;display:grid;place-items:center;flex:none;color:var(--muted);background:#101019;border:1px solid var(--border)}
.feed-ico.success{color:var(--success);background:var(--success-soft);border-color:rgba(62,207,142,0.20)}
.feed-ico.warning{color:var(--warning);background:var(--warning-soft);border-color:rgba(245,166,35,0.20)}
.feed-ico.danger{color:var(--danger);background:var(--danger-soft);border-color:rgba(240,82,82,0.20)}
.feed-ico.accent{color:var(--accent);background:var(--accent-soft);border-color:rgba(79,142,247,0.22)}
.feed-body{flex:1;min-width:0;font-size:12px}
.feed-body b{font-weight:500;color:var(--fg)}
.feed-body .target{font-family:var(--mono);color:var(--fg);background:#1d1d28;padding:1px 5px;border-radius:3px;font-size:11px;border:1px solid var(--border)}
.feed-meta{color:var(--muted);font-size:10.5px;margin-top:2px;font-family:var(--mono)}
/* Misc */
.scroll-x{overflow-x:auto}
.scroll-x::-webkit-scrollbar{height:8px}
.scroll-x::-webkit-scrollbar-thumb{background:#1d1d28;border-radius:4px}
.nav::-webkit-scrollbar{width:6px}
.nav::-webkit-scrollbar-thumb{background:#1d1d28;border-radius:3px}
/* ---------- Responsive ---------- */
@media (max-width:1380px){
.metrics{grid-template-columns:repeat(3,1fr)}
}
@media (max-width:1180px){
.bottom-row{grid-template-columns:1fr 1fr}
.bottom-row > :first-child{grid-column:1/-1}
}
@media (max-width:980px){
.app{grid-template-columns:64px 1fr}
.sidebar{padding:14px 8px}
.brand-name,.ws-meta,.ws-chev,.nav-title,.nav-item .count,.plan-box,.nav-item span:not(.ico-w){display:none}
.nav-item{justify-content:center;padding:8px}
.ws-switch{justify-content:center;padding:6px}
.search{min-width:160px}
}
@media (max-width:760px){
.metrics{grid-template-columns:repeat(2,1fr)}
.bottom-row{grid-template-columns:1fr}
.content{padding:16px}
.search{display:none}
}
</style>
</head>
<body>
<div class="app">
<!-- ============ SIDEBAR ============ -->
<aside class="sidebar">
<div class="brand">
<div class="brand-mark" aria-hidden="true"></div>
<div class="brand-name">CluPilot<span>v2.4</span></div>
</div>
<button class="ws-switch" aria-label="Workspace wechseln">
<div class="ws-avatar">AC</div>
<div class="ws-meta">
<div class="ws-name">Acme Cluster</div>
<div class="ws-plan">team · 14 user</div>
</div>
<svg class="ws-chev" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M8 9l4-4 4 4M8 15l4 4 4-4"/></svg>
</button>
<nav class="nav">
<div class="nav-section">
<div class="nav-title">Übersicht</div>
<div class="nav-item active">
<svg class="ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><rect x="3" y="3" width="7" height="9" rx="1.5"/><rect x="14" y="3" width="7" height="5" rx="1.5"/><rect x="14" y="12" width="7" height="9" rx="1.5"/><rect x="3" y="16" width="7" height="5" rx="1.5"/></svg>
<span>Dashboard</span>
</div>
<div class="nav-item">
<svg class="ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><path d="M3 12h4l3-7 4 14 3-7h4"/></svg>
<span>Metriken</span>
<span class="count">live</span>
</div>
<div class="nav-item">
<svg class="ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><circle cx="12" cy="12" r="9"/><path d="M12 7v5l3 2"/></svg>
<span>Aktivität</span>
</div>
</div>
<div class="nav-section">
<div class="nav-title">Infrastruktur</div>
<div class="nav-item">
<svg class="ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><rect x="3" y="4" width="18" height="7" rx="1.5"/><rect x="3" y="13" width="18" height="7" rx="1.5"/><circle cx="7" cy="7.5" r="0.7" fill="currentColor"/><circle cx="7" cy="16.5" r="0.7" fill="currentColor"/></svg>
<span>Server</span>
<span class="count">6</span>
</div>
<div class="nav-item">
<svg class="ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><path d="M5 12V7a3 3 0 0 1 3-3h8a3 3 0 0 1 3 3v5M3 12h18l-2 8H5l-2-8z"/></svg>
<span>Cluster</span>
</div>
<div class="nav-item">
<svg class="ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><path d="M9 17l-5 3v-7l5 3zM15 17l5 3v-7l-5 3zM12 4l5 3v6l-5 3-5-3V7l5-3z"/></svg>
<span>SSH Keys</span>
</div>
<div class="nav-item">
<svg class="ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><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"/></svg>
<span>DNS &amp; Domains</span>
</div>
</div>
<div class="nav-section">
<div class="nav-title">WordPress</div>
<div class="nav-item">
<svg class="ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><circle cx="12" cy="12" r="9"/><path d="M3 12h18M12 3v18"/></svg>
<span>Sites</span>
<span class="count">87</span>
</div>
<div class="nav-item">
<svg class="ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><path d="M12 4v16M4 12h16"/><circle cx="12" cy="12" r="9"/></svg>
<span>Plugins</span>
</div>
<div class="nav-item">
<svg class="ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><path d="M3 6h18M3 12h18M3 18h18"/></svg>
<span>Themes</span>
</div>
<div class="nav-item">
<svg class="ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><path d="M21 12c0 5-9 9-9 9s-9-4-9-9V5l9-3 9 3v7z"/><path d="M9 11l2 2 4-4"/></svg>
<span>Backups</span>
</div>
</div>
<div class="nav-section">
<div class="nav-title">Sicherheit</div>
<div class="nav-item">
<svg class="ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><path d="M12 3l8 4v5c0 5-3.5 8-8 9-4.5-1-8-4-8-9V7l8-4z"/></svg>
<span>Firewall</span>
<span class="count" style="color:var(--warning);background:var(--warning-soft);border-color:rgba(245,166,35,0.25)">3</span>
</div>
<div class="nav-item">
<svg class="ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><circle cx="12" cy="12" r="9"/><path d="M12 8v4l3 2"/></svg>
<span>Audit-Log</span>
</div>
<div class="nav-item">
<svg class="ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><rect x="5" y="11" width="14" height="9" rx="1.5"/><path d="M8 11V7a4 4 0 0 1 8 0v4"/></svg>
<span>Zertifikate</span>
</div>
</div>
<div class="nav-section">
<div class="nav-title">Einstellungen</div>
<div class="nav-item">
<svg class="ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.7 1.7 0 0 0 .3 1.8l.1.1a2 2 0 1 1-2.8 2.8l-.1-.1a1.7 1.7 0 0 0-1.8-.3 1.7 1.7 0 0 0-1 1.5V21a2 2 0 1 1-4 0v-.1a1.7 1.7 0 0 0-1.1-1.5 1.7 1.7 0 0 0-1.8.3l-.1.1a2 2 0 1 1-2.8-2.8l.1-.1a1.7 1.7 0 0 0 .3-1.8 1.7 1.7 0 0 0-1.5-1H3a2 2 0 1 1 0-4h.1A1.7 1.7 0 0 0 4.6 9a1.7 1.7 0 0 0-.3-1.8l-.1-.1a2 2 0 1 1 2.8-2.8l.1.1a1.7 1.7 0 0 0 1.8.3H9a1.7 1.7 0 0 0 1-1.5V3a2 2 0 1 1 4 0v.1a1.7 1.7 0 0 0 1 1.5 1.7 1.7 0 0 0 1.8-.3l.1-.1a2 2 0 1 1 2.8 2.8l-.1.1a1.7 1.7 0 0 0-.3 1.8V9a1.7 1.7 0 0 0 1.5 1H21a2 2 0 1 1 0 4h-.1a1.7 1.7 0 0 0-1.5 1z"/></svg>
<span>Allgemein</span>
</div>
<div class="nav-item">
<svg class="ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M22 21v-2a4 4 0 0 0-3-3.9M16 3.1a4 4 0 0 1 0 7.8"/></svg>
<span>Team</span>
</div>
<div class="nav-item">
<svg class="ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><rect x="3" y="6" width="18" height="13" rx="2"/><path d="M3 10h18"/></svg>
<span>Abrechnung</span>
</div>
</div>
</nav>
<div class="plan-box">
<div class="plan-row">
<div class="plan-name">Team Plan</div>
<div class="plan-tag">PRO</div>
</div>
<div class="plan-meter"><span></span></div>
<div class="plan-stats">
<span>87 / 140 sites</span>
<span>62%</span>
</div>
<button class="plan-cta">Upgrade auf Scale →</button>
</div>
</aside>
<!-- ============ MAIN ============ -->
<main class="main">
<!-- Topbar -->
<div class="topbar">
<div class="crumbs">
<span class="crumb">Acme Cluster</span>
<span class="crumb sep">/</span>
<span class="page-title">Dashboard</span>
</div>
<span class="status-chip"><span class="pulse-dot"></span> All systems operational</span>
<div class="top-spacer"></div>
<div class="search">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="7"/><path d="M21 21l-4.3-4.3"/></svg>
<input placeholder="Sites, Server, Domains suchen…" />
<span class="kbd">⌘K</span>
</div>
<button class="icon-btn" aria-label="Dokumentation">
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="9"/><path d="M9.1 9a3 3 0 0 1 5.8 1c0 2-3 3-3 3M12 17h.01"/></svg>
</button>
<button class="icon-btn" aria-label="Benachrichtigungen">
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M6 8a6 6 0 0 1 12 0c0 7 3 9 3 9H3s3-2 3-9M10 21a2 2 0 0 0 4 0"/></svg>
<span class="badge">3</span>
</button>
<button class="btn-primary">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="M12 5v14M5 12h14"/></svg>
Server
</button>
</div>
<div class="content">
<!-- ============ METRICS ============ -->
<div class="metrics">
<div class="metric success">
<div class="metric-label">
<svg class="ico" 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>
Sites Online
</div>
<div class="metric-value">87<span class="sub">/87</span></div>
<div class="metric-delta"><span class="chg up">100%</span> Verfügbarkeit · 24h</div>
<svg class="spark" width="60" height="22" viewBox="0 0 60 22"><polyline fill="none" stroke="#3ecf8e" stroke-width="1.5" points="0,16 8,14 16,15 24,12 32,13 40,10 48,11 60,8"/></svg>
</div>
<div class="metric">
<div class="metric-label">
<svg class="ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="9"/><path d="M12 7v5l3 2"/></svg>
Ø Response
</div>
<div class="metric-value">142<span class="sub">ms</span></div>
<div class="metric-delta"><span class="chg up">↓ 8.4%</span> vs. letzte Woche</div>
<svg class="spark" width="60" height="22" viewBox="0 0 60 22"><polyline fill="none" stroke="#4f8ef7" stroke-width="1.5" points="0,8 8,12 16,9 24,14 32,11 40,10 48,7 60,9"/></svg>
</div>
<div class="metric warning">
<div class="metric-label">
<svg class="ico" 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>
Updates Pending
</div>
<div class="metric-value">24</div>
<div class="metric-delta"><span class="chg warn">7 kritisch</span> · 17 minor</div>
<svg class="spark" width="60" height="22" viewBox="0 0 60 22"><polyline fill="none" stroke="#f5a623" stroke-width="1.5" points="0,12 8,14 16,11 24,9 32,10 40,8 48,6 60,5"/></svg>
</div>
<div class="metric success">
<div class="metric-label">
<svg class="ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 12c0 5-9 9-9 9s-9-4-9-9V5l9-3 9 3v7z"/><path d="M9 11l2 2 4-4"/></svg>
Backups 24h
</div>
<div class="metric-value">86<span class="sub">/87</span></div>
<div class="metric-delta"><span class="chg warn">1 fehlgeschlagen</span> · 02:14 UTC</div>
<svg class="spark" width="60" height="22" viewBox="0 0 60 22"><polyline fill="none" stroke="#3ecf8e" stroke-width="1.5" points="0,6 8,5 16,6 24,5 32,6 40,5 48,6 60,5"/></svg>
</div>
<div class="metric danger">
<div class="metric-label">
<svg class="ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 3l8 4v5c0 5-3.5 8-8 9-4.5-1-8-4-8-9V7l8-4z"/><path d="M12 8v5M12 16h.01"/></svg>
Security Events
</div>
<div class="metric-value">128</div>
<div class="metric-delta"><span class="chg down">↑ 23%</span> Brute-force Attempts blockiert</div>
<svg class="spark" width="60" height="22" viewBox="0 0 60 22"><polyline fill="none" stroke="#f05252" stroke-width="1.5" points="0,16 8,12 16,14 24,9 32,11 40,6 48,8 60,4"/></svg>
</div>
<div class="metric">
<div class="metric-label">
<svg class="ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><ellipse cx="12" cy="6" rx="9" ry="3"/><path d="M3 6v12c0 1.7 4 3 9 3s9-1.3 9-3V6M3 12c0 1.7 4 3 9 3s9-1.3 9-3"/></svg>
Storage
</div>
<div class="metric-value">1.84<span class="sub">TB</span></div>
<div class="metric-delta"><span class="chg warn">73%</span> von 2.5 TB belegt</div>
<svg class="spark" width="60" height="22" viewBox="0 0 60 22"><polyline fill="none" stroke="#8888a0" stroke-width="1.5" points="0,14 8,13 16,12 24,11 32,10 40,9 48,8 60,7"/></svg>
</div>
</div>
<!-- ============ SERVER TABLE ============ -->
<section class="card">
<div class="card-head">
<h3>
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="4" width="18" height="7" rx="1.5"/><rect x="3" y="13" width="18" height="7" rx="1.5"/><circle cx="7" cy="7.5" r="0.7" fill="currentColor"/><circle cx="7" cy="16.5" r="0.7" fill="currentColor"/></svg>
Server-Flotte
<span class="pill muted" style="margin-left:6px">6 nodes</span>
</h3>
<div class="card-tabs">
<button class="active">Alle</button>
<button>Produktion</button>
<button>Staging</button>
<button>Edge</button>
</div>
</div>
<div class="scroll-x">
<table class="table">
<thead>
<tr>
<th>Server</th>
<th>Sites</th>
<th>CPU · RAM · Disk</th>
<th>Standort</th>
<th class="num">Uptime</th>
<th class="num" style="width:120px"></th>
</tr>
</thead>
<tbody id="server-body"></tbody>
</table>
</div>
</section>
<!-- ============ SITES TABLE ============ -->
<section class="card">
<div class="card-head">
<h3>
<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="M3 12h18M12 3a14 14 0 0 1 0 18M12 3a14 14 0 0 0 0 18"/></svg>
WordPress-Sites
<span class="pill muted" style="margin-left:6px">8 sichtbar · 87 gesamt</span>
</h3>
<div class="section-actions">
<button class="ghost-btn">Filter</button>
<button class="ghost-btn">Sortieren</button>
<button class="ghost-btn active">Alle anzeigen →</button>
</div>
</div>
<div class="scroll-x">
<table class="table">
<thead>
<tr>
<th>Site</th>
<th>Server</th>
<th>WP</th>
<th>PHP</th>
<th class="num">Visitors / 24h</th>
<th>Updates</th>
<th>Backup</th>
<th>Security</th>
<th class="num"></th>
</tr>
</thead>
<tbody id="sites-body"></tbody>
</table>
</div>
</section>
<!-- ============ BOTTOM ROW ============ -->
<div class="bottom-row">
<!-- Traffic chart -->
<section class="card">
<div class="card-head">
<h3>
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 17l5-5 4 4 7-8M14 8h6v6"/></svg>
Traffic
<span style="font-size:10.5px;color:var(--success);font-family:var(--mono);display:inline-flex;align-items:center;gap:5px;font-weight:500;margin-left:2px"><span class="pulse-dot" style="color:var(--success)"></span>live</span>
</h3>
<div class="card-tabs">
<button>15m</button>
<button class="active">60m</button>
<button>24h</button>
<button>7d</button>
</div>
</div>
<div class="chart-wrap">
<div class="chart-head">
<div>
<div class="chart-figure">14.2k<span class="unit">req/min</span></div>
<div class="chart-sub">Peak vor 12 min · 16.8k req/min</div>
</div>
<div class="legend">
<span><i style="background:#4f8ef7"></i>Requests</span>
<span><i style="background:#7c5cff"></i>Cache hit</span>
</div>
</div>
<svg class="svg-chart" viewBox="0 0 600 160" preserveAspectRatio="none">
<defs>
<linearGradient id="grad-req" x1="0" x2="0" y1="0" y2="1">
<stop offset="0%" stop-color="#4f8ef7" stop-opacity="0.35"/>
<stop offset="100%" stop-color="#4f8ef7" stop-opacity="0"/>
</linearGradient>
<linearGradient id="grad-cache" x1="0" x2="0" y1="0" y2="1">
<stop offset="0%" stop-color="#7c5cff" stop-opacity="0.20"/>
<stop offset="100%" stop-color="#7c5cff" stop-opacity="0"/>
</linearGradient>
</defs>
<!-- grid -->
<g stroke="rgba(255,255,255,0.04)" stroke-width="1">
<line x1="0" y1="30" x2="600" y2="30"/>
<line x1="0" y1="65" x2="600" y2="65"/>
<line x1="0" y1="100" x2="600" y2="100"/>
<line x1="0" y1="135" x2="600" y2="135"/>
</g>
<!-- y axis labels -->
<text class="chart-axis" x="4" y="34">20k</text>
<text class="chart-axis" x="4" y="69">15k</text>
<text class="chart-axis" x="4" y="104">10k</text>
<text class="chart-axis" x="4" y="139">5k</text>
<!-- cache area -->
<path id="cache-area" d="" fill="url(#grad-cache)" />
<path id="cache-line" d="" stroke="#7c5cff" stroke-width="1.6" fill="none" stroke-linecap="round" stroke-linejoin="round"/>
<!-- req area -->
<path id="req-area" d="" fill="url(#grad-req)" />
<path id="req-line" d="" stroke="#4f8ef7" stroke-width="1.8" fill="none" stroke-linecap="round" stroke-linejoin="round"/>
<!-- live dot -->
<circle id="live-dot" cx="0" cy="0" r="3.5" fill="#4f8ef7" stroke="#0a0a0f" stroke-width="2"/>
<!-- x axis labels -->
<text class="chart-axis" x="40" y="156">-60m</text>
<text class="chart-axis" x="180" y="156">-45m</text>
<text class="chart-axis" x="320" y="156">-30m</text>
<text class="chart-axis" x="460" y="156">-15m</text>
<text class="chart-axis" x="568" y="156">jetzt</text>
</svg>
</div>
</section>
<!-- Update pipeline -->
<section class="card">
<div class="card-head">
<h3>
<svg width="14" height="14" 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>
Update-Pipeline
</h3>
<span class="meta">5 aktiv · 19 wartend</span>
</div>
<div class="pipe" id="pipe-body"></div>
</section>
<!-- Activity feed -->
<section class="card">
<div class="card-head">
<h3>
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M22 12h-4l-3 9L9 3l-3 9H2"/></svg>
Live Activity
<span style="font-size:10.5px;color:var(--success);font-family:var(--mono);display:inline-flex;align-items:center;gap:5px;font-weight:500;margin-left:2px"><span class="pulse-dot" style="color:var(--success)"></span>live</span>
</h3>
<span class="meta">letzte 30 min</span>
</div>
<div class="feed" id="feed-body"></div>
</section>
</div>
<div style="text-align:center;color:var(--muted-2);font-size:11px;font-family:var(--mono);padding:6px 0 20px;letter-spacing:0.04em">
CluPilot · region: eu-central-1 · build #4f8a32c · uptime 99.984%
</div>
</div>
</main>
</div>
<script>
/* ============ ICONS (inline) ============ */
const Icon = {
terminal:'<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>',
restart:'<svg width="14" height="14" 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>',
more:'<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="5" cy="12" r="1.5"/><circle cx="12" cy="12" r="1.5"/><circle cx="19" cy="12" r="1.5"/></svg>',
ext:'<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>',
server:'<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="4" width="18" height="7" rx="1.5"/><rect x="3" y="13" width="18" height="7" rx="1.5"/><circle cx="7" cy="7.5" r="0.7" fill="currentColor"/><circle cx="7" cy="16.5" r="0.7" fill="currentColor"/></svg>',
};
/* ============ HELPERS ============ */
const statusClass = v => v < 60 ? 'ok' : v < 80 ? 'warn' : 'bad';
const statusPillClass = v => v < 60 ? 'success' : v < 80 ? 'warning' : 'danger';
/* ============ SERVERS ============ */
const servers = [
{name:'eu-prod-01', ip:'185.243.117.42', sites:23, cpu:42, ram:58, disk:71, location:'Frankfurt 🇩🇪', flag:'FRA', uptime:'127d 14h', state:'up', host:'Hetzner · CX42'},
{name:'eu-prod-02', ip:'185.243.117.43', sites:19, cpu:67, ram:74, disk:55, location:'Frankfurt 🇩🇪', flag:'FRA', uptime:'127d 14h', state:'up', host:'Hetzner · CX42'},
{name:'eu-edge-amsterdam', ip:'78.46.201.18', sites:14, cpu:31, ram:44, disk:38, location:'Amsterdam 🇳🇱', flag:'AMS', uptime:'89d 02h', state:'up', host:'Hetzner · CX32'},
{name:'us-east-prod-01', ip:'5.161.42.198', sites:21, cpu:78, ram:81, disk:62, location:'Ashburn 🇺🇸', flag:'IAD', uptime:'214d 06h', state:'warn', host:'Hetzner · CCX23'},
{name:'eu-staging', ip:'168.119.84.221', sites:6, cpu:18, ram:29, disk:22, location:'Nürnberg 🇩🇪', flag:'NBG', uptime:'43d 11h', state:'up', host:'Hetzner · CX22'},
{name:'apac-sydney-01', ip:'209.97.165.88', sites:4, cpu:54, ram:51, disk:84, location:'Sydney 🇦🇺', flag:'SYD', uptime:'62d 19h', state:'warn', host:'DigitalOcean'},
];
const serverBody = document.getElementById('server-body');
servers.forEach(s => {
const cpuC = statusClass(s.cpu), ramC = statusClass(s.ram), diskC = statusClass(s.disk);
const stateIcon = s.state==='up'
? '<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M5 12l5 5L20 7"/></svg>'
: '<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 9v4M12 17h.01"/><circle cx="12" cy="12" r="9"/></svg>';
serverBody.insertAdjacentHTML('beforeend',`
<tr>
<td>
<div class="server-cell">
<div class="server-icon ${s.state==='up'?'up':'warn'}">${stateIcon}</div>
<div class="server-meta">
<div class="server-name">${s.name}</div>
<div class="server-ip">${s.ip} · ${s.host}</div>
</div>
</div>
</td>
<td><span class="pill">${s.sites} sites</span></td>
<td>
<div class="res-grid">
<div class="res">
<div class="res-top"><span>CPU</span><span class="val">${s.cpu}%</span></div>
<div class="res-bar"><div class="res-fill ${cpuC}" style="width:${s.cpu}%"></div></div>
</div>
<div class="res">
<div class="res-top"><span>RAM</span><span class="val">${s.ram}%</span></div>
<div class="res-bar"><div class="res-fill ${ramC}" style="width:${s.ram}%"></div></div>
</div>
<div class="res">
<div class="res-top"><span>Disk</span><span class="val">${s.disk}%</span></div>
<div class="res-bar"><div class="res-fill ${diskC}" style="width:${s.disk}%"></div></div>
</div>
</div>
</td>
<td><span style="color:var(--fg)">${s.location.replace(/(.+) (.+)/,'$1')}</span> <span class="pill muted" style="margin-left:6px">${s.flag}</span></td>
<td class="num"><span style="color:var(--fg)">${s.uptime}</span></td>
<td class="num">
<div class="row-actions">
<button title="SSH-Terminal">${Icon.terminal}</button>
<button title="Neustart">${Icon.restart}</button>
<button title="Öffnen">${Icon.ext}</button>
<button title="Mehr">${Icon.more}</button>
</div>
</td>
</tr>`);
});
/* ============ SITES ============ */
const palette = ['#4f8ef7','#3ecf8e','#f5a623','#7c5cff','#f05252','#22c1d6','#e879f9','#facc15'];
const sites = [
{url:'kunde-mueller.de', server:'eu-prod-01', wp:'6.5.2', wpOk:true, php:'8.2', visitors:'12.4k', updates:0, backup:'ok', grade:'A'},
{url:'shop.bergmann-coffee.com', server:'eu-prod-01', wp:'6.4.3', wpOk:false, php:'8.1', visitors:'48.7k', updates:3, backup:'ok', grade:'A'},
{url:'blog.fintech-radar.io', server:'eu-prod-02', wp:'6.5.2', wpOk:true, php:'8.3', visitors:'9.1k', updates:1, backup:'ok', grade:'B'},
{url:'praxis-dr-schwarz.de', server:'eu-prod-02', wp:'6.2.1', wpOk:false, php:'7.4', visitors:'1.2k', updates:7, backup:'warn', grade:'D'},
{url:'agentur-helio.com', server:'eu-edge-amsterdam', wp:'6.5.2', wpOk:true, php:'8.2', visitors:'18.2k', updates:0, backup:'ok', grade:'A'},
{url:'docs.opencrate.dev', server:'us-east-prod-01', wp:'6.5.1', wpOk:true, php:'8.3', visitors:'7.8k', updates:2, backup:'ok', grade:'B'},
{url:'staging.opencrate.dev', server:'eu-staging', wp:'6.6-RC1', wpOk:true, php:'8.3', visitors:'82', updates:0, backup:'ok', grade:'B'},
{url:'classic.tonhoff-bau.de', server:'us-east-prod-01', wp:'5.9.7', wpOk:false, php:'7.4', visitors:'3.4k', updates:11,backup:'err', grade:'F'},
];
const sitesBody = document.getElementById('sites-body');
sites.forEach((s,i)=>{
const initial = s.url[0].toUpperCase();
const color = palette[i % palette.length];
const wpPill = s.wpOk ? 'success' : 'warning';
const phpOld = parseFloat(s.php) < 8.0;
const phpPill = phpOld ? 'warning' : 'muted';
const updPill = s.updates===0 ? 'success' : s.updates>=5 ? 'danger' : 'warning';
const updLabel = s.updates===0 ? 'aktuell' : `${s.updates} verfügbar`;
const backupMap = {
ok: {cls:'success', dot:'✓', label:'vor 2 h'},
warn: {cls:'warning', dot:'!', label:'vor 18 h'},
err: {cls:'danger', dot:'×', label:'fehlgeschl.'},
}[s.backup];
const grade = s.grade.toLowerCase();
sitesBody.insertAdjacentHTML('beforeend',`
<tr>
<td>
<div class="site-cell">
<div class="favicon" style="background:${color}">${initial}</div>
<div style="min-width:0">
<div class="site-name">${s.url}</div>
<div class="site-meta">id_${(7300+i).toString(16)} · ssl ✓</div>
</div>
</div>
</td>
<td><span class="pill muted">${s.server}</span></td>
<td><span class="pill ${wpPill}">WP ${s.wp}</span></td>
<td><span class="pill ${phpPill}">PHP ${s.php}</span></td>
<td class="num"><span style="color:var(--fg)">${s.visitors}</span></td>
<td><span class="pill ${updPill}">${updLabel}</span></td>
<td>
<span class="pill ${backupMap.cls}">
<span style="display:inline-block;width:5px;height:5px;border-radius:50%;background:currentColor"></span>
${backupMap.label}
</span>
</td>
<td><span class="grade ${grade}">${s.grade}</span></td>
<td class="num">
<div class="row-actions">
<button title="Site öffnen">${Icon.ext}</button>
<button title="WP-Admin">${Icon.terminal}</button>
<button title="Mehr">${Icon.more}</button>
</div>
</td>
</tr>`);
});
/* ============ UPDATE PIPELINE ============ */
const pipe = [
{type:'wp', title:'WordPress 6.5.2 → 6.5.3', sub:'agentur-helio.com', pct:78, status:'läuft', statusCls:'accent'},
{type:'pl', title:'WooCommerce 8.7 → 8.8.1', sub:'shop.bergmann-coffee.com', pct:42, status:'läuft', statusCls:'accent'},
{type:'pl', title:'Yoast SEO 22.4 → 22.5', sub:'blog.fintech-radar.io', pct:100,status:'erledigt', statusCls:'success'},
{type:'php', title:'PHP 7.4 → 8.2 Migration', sub:'praxis-dr-schwarz.de', pct:12, status:'review', statusCls:'warning'},
{type:'wp', title:'WordPress 5.9 → 6.5.2', sub:'classic.tonhoff-bau.de', pct:0, status:'fehler', statusCls:'danger'},
{type:'pl', title:'Wordfence 7.11 → 7.12.0', sub:'+ 14 sites batch', pct:0, status:'wartet', statusCls:'muted'},
];
const pipeBody = document.getElementById('pipe-body');
pipe.forEach(p=>{
pipeBody.insertAdjacentHTML('beforeend',`
<div class="pipe-row">
<div class="pipe-ico ${p.type}">${p.type.toUpperCase()}</div>
<div class="pipe-info">
<div class="pipe-title">${p.title}</div>
<div class="pipe-sub">${p.sub}</div>
</div>
<div style="display:flex;flex-direction:column;align-items:flex-end;gap:4px">
<span class="pill ${p.statusCls}">${p.pct>0&&p.pct<100?p.pct+'%':''} ${p.status}</span>
${p.pct>0 ? `<div class="mini-bar"><span style="width:${p.pct}%;background:${p.statusCls==='success'?'var(--success)':p.statusCls==='warning'?'var(--warning)':p.statusCls==='danger'?'var(--danger)':'var(--accent)'}"></span></div>` : ''}
</div>
</div>`);
});
/* ============ ACTIVITY FEED ============ */
const feed = [
{ic:'success', icon:'<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2"><path d="M5 12l5 5L20 7"/></svg>',
html:'Backup erstellt für <span class="target">agentur-helio.com</span>', meta:'vor 12 sek · 248 MB · eu-prod-02'},
{ic:'danger', icon:'<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2"><path d="M12 3l8 4v5c0 5-3.5 8-8 9-4.5-1-8-4-8-9V7l8-4z"/></svg>',
html:'<b>17 Login-Versuche</b> blockiert auf <span class="target">classic.tonhoff-bau.de</span>', meta:'vor 1 min · IP 185.220.101.18 · TOR exit'},
{ic:'accent', icon:'<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2"><path d="M21 12a9 9 0 1 1-3-6.7M21 4v5h-5"/></svg>',
html:'Plugin-Update <span class="target">Yoast SEO 22.5</span> erfolgreich auf 3 Sites', meta:'vor 4 min · ausgeführt von <b>m.weber</b>'},
{ic:'warning', icon:'<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2"><path d="M12 9v4M12 17h.01"/><circle cx="12" cy="12" r="9"/></svg>',
html:'CPU > 80% auf <span class="target">us-east-prod-01</span>', meta:'vor 7 min · sustained 4m 12s'},
{ic:'success', icon:'<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2"><rect x="5" y="11" width="14" height="9" rx="1.5"/><path d="M8 11V7a4 4 0 0 1 8 0v4"/></svg>',
html:'SSL-Zertifikat erneuert · <span class="target">*.kunde-mueller.de</span>', meta:'vor 14 min · gültig bis 25.08.2026'},
{ic:'accent', icon:'<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2"><path d="M4 17l5-5-5-5M13 19h7"/></svg>',
html:'SSH-Session geöffnet auf <span class="target">eu-prod-01</span>', meta:'vor 19 min · user <b>j.koch</b> · key id_ed25519_jk'},
{ic:'success', icon:'<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2"><path d="M5 12l5 5L20 7"/></svg>',
html:'Health-Check OK · <b>87/87 Sites</b>', meta:'vor 23 min · run #4421'},
{ic:'warning', icon:'<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2"><path d="M21 12c0 5-9 9-9 9s-9-4-9-9V7l9-3 9 3v7z"/></svg>',
html:'Backup verspätet · <span class="target">staging.opencrate.dev</span>', meta:'vor 27 min · retry geplant'},
];
const feedBody = document.getElementById('feed-body');
feed.forEach(f=>{
feedBody.insertAdjacentHTML('beforeend',`
<div class="feed-row">
<div class="feed-ico ${f.ic}">${f.icon}</div>
<div class="feed-body">
<div>${f.html}</div>
<div class="feed-meta">${f.meta}</div>
</div>
</div>`);
});
/* ============ TRAFFIC CHART (smooth procedural) ============ */
(function(){
const W = 600, H = 160, padL = 30, padR = 8, padT = 10, padB = 28;
const N = 60;
const reqData = [], cacheData = [];
// generate plausible req-per-min around 14k with peaks
let v = 11; // in k
for(let i=0;i<N;i++){
v += (Math.random()-0.45)*1.6;
// peak around index 48 (12 min ago)
if(i>40 && i<52) v += Math.sin((i-40)/12*Math.PI)*3.5;
v = Math.max(5, Math.min(19, v));
reqData.push(v);
cacheData.push(v * (0.55 + Math.random()*0.15));
}
const x = i => padL + (W-padL-padR) * (i/(N-1));
const y = vv => {
// scale 0-20 → padT..H-padB
const t = vv/20;
return H - padB - t*(H-padT-padB);
};
function pathFor(arr){
let d = `M ${x(0)} ${y(arr[0])}`;
for(let i=1;i<arr.length;i++){
const xc = (x(i-1)+x(i))/2;
d += ` Q ${x(i-1)} ${y(arr[i-1])} ${xc} ${(y(arr[i-1])+y(arr[i]))/2}`;
}
d += ` T ${x(arr.length-1)} ${y(arr[arr.length-1])}`;
return d;
}
function areaFor(arr){
return pathFor(arr) + ` L ${x(arr.length-1)} ${H-padB} L ${x(0)} ${H-padB} Z`;
}
document.getElementById('req-line').setAttribute('d', pathFor(reqData));
document.getElementById('req-area').setAttribute('d', areaFor(reqData));
document.getElementById('cache-line').setAttribute('d', pathFor(cacheData));
document.getElementById('cache-area').setAttribute('d', areaFor(cacheData));
const dot = document.getElementById('live-dot');
dot.setAttribute('cx', x(N-1));
dot.setAttribute('cy', y(reqData[N-1]));
// gentle pulse
let t0 = 0;
function tick(){
t0 += 0.04;
const r = 3.5 + Math.sin(t0)*0.8;
dot.setAttribute('r', r);
requestAnimationFrame(tick);
}
tick();
})();
</script>
</body>
</html>

View File

@ -0,0 +1,413 @@
<!doctype html>
<html lang="de">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>Anmelden · CluPilot</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet">
<style>
:root{
--bg:#0a0a0f;
--bg-2:#0c0c12;
--card:#16161e;
--card-hover:#1a1a24;
--surface:#101019;
--surface-2:#1d1d28;
--border:rgba(255,255,255,0.06);
--border-strong:rgba(255,255,255,0.10);
--border-focus:rgba(79,142,247,0.5);
--fg:#f0f0f5;
--muted:#8888a0;
--muted-2:#5c5c70;
--accent:#4f8ef7;
--accent-2:#7c5cff;
--accent-soft:rgba(79,142,247,0.12);
--success:#3ecf8e;
--warning:#f5a623;
--danger:#f05252;
--success-soft:rgba(62,207,142,0.12);
--mono:'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, monospace;
}
*{box-sizing:border-box}
html,body{margin:0;padding:0;background:var(--bg);color:var(--fg);font-family:'Inter',system-ui,-apple-system,sans-serif;font-size:13.5px;line-height:1.5;-webkit-font-smoothing:antialiased;min-height:100vh}
a{color:var(--accent);text-decoration:none}
a:hover{text-decoration:underline}
button{font-family:inherit;color:inherit;background:none;border:0;cursor:pointer}
/* ===== ambient backdrop ===== */
body::before{
content:"";
position:fixed;inset:0;z-index:0;pointer-events:none;
background:
radial-gradient(60% 50% at 12% 10%, rgba(79,142,247,0.10), transparent 65%),
radial-gradient(50% 50% at 88% 100%, rgba(124,92,255,0.08), transparent 65%);
}
body::after{
content:"";
position:fixed;inset:0;z-index:0;pointer-events:none;
background-image:
linear-gradient(rgba(255,255,255,0.018) 1px, transparent 1px),
linear-gradient(90deg, rgba(255,255,255,0.018) 1px, transparent 1px);
background-size:32px 32px;
mask-image:radial-gradient(ellipse at center, #000 30%, transparent 75%);
}
/* ===== layout ===== */
.page{position:relative;z-index:1;min-height:100vh;display:grid;grid-template-columns:minmax(420px,1fr) 1fr;gap:0}
/* ===== left: form ===== */
.form-side{display:flex;flex-direction:column;padding:32px 48px;min-height:100vh}
.top-row{display:flex;align-items:center;justify-content:space-between;gap:16px}
.brand{display:flex;align-items:center;gap:10px}
.brand-mark{width:30px;height:30px;border-radius:8px;background:linear-gradient(135deg,#4f8ef7 0%,#7c5cff 100%);position:relative;display:grid;place-items:center;box-shadow:0 0 0 1px rgba(255,255,255,0.06) inset, 0 6px 16px rgba(79,142,247,0.30)}
.brand-mark::before{content:"";width:11px;height:11px;border-radius:50%;background:#0a0a0f;box-shadow:inset 0 0 0 2px #fff}
.brand-name{font-weight:600;font-size:15px;letter-spacing:-0.01em}
.brand-name span{color:var(--muted);font-weight:500;margin-left:5px;font-size:11.5px}
.status-chip{display:inline-flex;align-items:center;gap:6px;padding:4px 9px 4px 8px;border:1px solid rgba(62,207,142,0.22);background:var(--success-soft);color:var(--success);border-radius:20px;font-size:11px;font-weight:500;font-family:var(--mono)}
.pulse-dot{width:6px;height:6px;border-radius:50%;background:currentColor;position:relative;flex:none}
.pulse-dot::after{content:"";position:absolute;inset:-3px;border-radius:50%;background:currentColor;opacity:0.35;animation:pulse 1.8s ease-out infinite}
@keyframes pulse{0%{transform:scale(0.6);opacity:.5}100%{transform:scale(2.4);opacity:0}}
.form-wrap{margin:auto 0;display:flex;flex-direction:column;gap:24px;max-width:380px;width:100%;padding:48px 0}
h1{margin:0;font-size:24px;font-weight:600;letter-spacing:-0.02em;line-height:1.2}
.sub{margin:6px 0 0;font-size:13.5px;color:var(--muted)}
.sub a{font-weight:500}
/* SSO row */
.sso-row{display:grid;grid-template-columns:1fr 1fr;gap:8px}
.sso-btn{display:flex;align-items:center;justify-content:center;gap:8px;padding:9px 12px;background:var(--surface);border:1px solid var(--border);border-radius:8px;color:var(--fg);font-size:12.5px;font-weight:500;transition:all .15s}
.sso-btn:hover{background:var(--surface-2);border-color:var(--border-strong)}
.sso-btn svg{flex:none}
.divider{display:flex;align-items:center;gap:12px;color:var(--muted-2);font-size:11px;font-family:var(--mono);text-transform:uppercase;letter-spacing:0.08em}
.divider::before,.divider::after{content:"";flex:1;height:1px;background:var(--border)}
/* Inputs */
form{display:flex;flex-direction:column;gap:14px}
.field{display:flex;flex-direction:column;gap:6px}
.field-label{display:flex;align-items:center;justify-content:space-between;font-size:11.5px;color:var(--muted);font-weight:500}
.field-label a{font-size:11.5px;color:var(--muted);font-weight:500}
.field-label a:hover{color:var(--fg);text-decoration:none}
.input{display:flex;align-items:center;gap:10px;padding:0 12px;background:var(--surface);border:1px solid var(--border);border-radius:8px;transition:border-color .15s, background .15s, box-shadow .15s;height:40px}
.input:hover{border-color:var(--border-strong)}
.input:focus-within{border-color:var(--border-focus);background:#12121c;box-shadow:0 0 0 3px rgba(79,142,247,0.10)}
.input svg{color:var(--muted-2);flex:none}
.input input{flex:1;background:none;border:0;outline:0;color:var(--fg);font-size:13.5px;font-family:inherit;min-width:0;height:100%}
.input input::placeholder{color:var(--muted-2)}
.input .eye{color:var(--muted);padding:4px;border-radius:4px;display:grid;place-items:center;transition:color .15s,background .15s}
.input .eye:hover{color:var(--fg);background:var(--surface-2)}
/* Remember + submit */
.row-between{display:flex;align-items:center;justify-content:space-between;margin-top:2px}
.checkbox{display:flex;align-items:center;gap:8px;font-size:12.5px;color:var(--muted);cursor:pointer;user-select:none}
.checkbox input{display:none}
.checkbox .box{width:15px;height:15px;border-radius:4px;border:1px solid var(--border-strong);background:var(--surface);display:grid;place-items:center;transition:all .15s}
.checkbox input:checked + .box{background:var(--accent);border-color:var(--accent)}
.checkbox input:checked + .box svg{opacity:1;transform:scale(1)}
.checkbox .box svg{opacity:0;transform:scale(0.6);transition:all .15s;color:#fff}
.checkbox:hover{color:var(--fg)}
.btn-primary{display:inline-flex;align-items:center;justify-content:center;gap:8px;padding:10px 16px;background:var(--accent);color:#fff;border-radius:8px;font-weight:500;font-size:13.5px;height:40px;transition:background .15s,transform .05s;box-shadow:0 1px 0 rgba(255,255,255,0.08) inset, 0 1px 3px rgba(79,142,247,0.35)}
.btn-primary:hover{background:#5e98f7}
.btn-primary:active{transform:translateY(1px)}
.btn-primary .arrow{transition:transform .15s}
.btn-primary:hover .arrow{transform:translateX(2px)}
/* Magic link / passkey link */
.alt-options{display:flex;flex-direction:column;gap:8px;margin-top:4px}
.alt-btn{display:flex;align-items:center;gap:10px;padding:9px 12px;background:transparent;border:1px solid var(--border);border-radius:8px;font-size:12.5px;color:var(--fg);font-weight:500;transition:all .15s}
.alt-btn:hover{border-color:var(--border-strong);background:var(--surface)}
.alt-btn .badge{margin-left:auto;font-family:var(--mono);font-size:10px;color:var(--accent);background:var(--accent-soft);padding:2px 6px;border-radius:4px;border:1px solid rgba(79,142,247,0.25);font-weight:500}
.alt-btn .ico{color:var(--muted)}
/* footer */
.form-footer{display:flex;align-items:center;justify-content:space-between;gap:12px;color:var(--muted);font-size:11.5px;font-family:var(--mono);padding-top:12px;border-top:1px solid var(--border);flex-wrap:wrap}
.form-footer .links{display:flex;gap:14px}
.form-footer .links a{color:var(--muted)}
.form-footer .links a:hover{color:var(--fg);text-decoration:none}
.form-footer .lock{display:flex;align-items:center;gap:6px}
/* ===== right: live panel ===== */
.live-side{position:relative;border-left:1px solid var(--border);background:linear-gradient(180deg,#0c0c12 0%, #0a0a0f 100%);padding:32px 36px;overflow:hidden;display:flex;flex-direction:column;gap:18px;min-height:100vh}
.live-side::before{
content:"";position:absolute;inset:0;pointer-events:none;
background:radial-gradient(80% 50% at 100% 0%, rgba(79,142,247,0.12), transparent 65%);
}
.live-side > *{position:relative}
.live-head{display:flex;align-items:center;justify-content:space-between}
.live-head .label{font-size:11px;color:var(--muted);font-family:var(--mono);text-transform:uppercase;letter-spacing:0.10em}
.live-head .meta{font-size:11px;color:var(--muted);font-family:var(--mono)}
.hero-quote{margin:8px 0 0;font-size:22px;font-weight:600;letter-spacing:-0.015em;line-height:1.3;max-width:480px}
.hero-quote em{font-style:normal;color:var(--accent);font-weight:600}
.hero-sub{color:var(--muted);font-size:13px;max-width:460px;margin-top:6px}
/* Stat strip */
.stat-strip{display:grid;grid-template-columns:repeat(3,1fr);gap:10px;margin-top:6px}
.stat{background:var(--card);border:1px solid var(--border);border-radius:9px;padding:12px}
.stat-label{font-size:10.5px;color:var(--muted);text-transform:uppercase;letter-spacing:0.06em;font-weight:500}
.stat-value{font-family:var(--mono);font-size:18px;font-weight:600;letter-spacing:-0.01em;margin-top:4px}
.stat-value .unit{font-size:12px;color:var(--muted);font-weight:500;margin-left:2px}
.stat-trend{display:inline-flex;align-items:center;gap:4px;font-family:var(--mono);font-size:10.5px;color:var(--success);margin-top:4px}
.stat-trend.warn{color:var(--warning)}
/* terminal card */
.term-card{background:var(--card);border:1px solid var(--border);border-radius:10px;overflow:hidden;flex:1;display:flex;flex-direction:column;min-height:280px}
.term-head{display:flex;align-items:center;gap:10px;padding:10px 14px;border-bottom:1px solid var(--border);background:#101019}
.term-dots{display:flex;gap:5px}
.term-dots span{width:9px;height:9px;border-radius:50%;background:#2a2a36}
.term-dots span:nth-child(1){background:#f05252}
.term-dots span:nth-child(2){background:#f5a623}
.term-dots span:nth-child(3){background:#3ecf8e}
.term-title{font-family:var(--mono);font-size:11.5px;color:var(--muted);flex:1;text-align:center}
.term-meta{font-family:var(--mono);font-size:10.5px;color:var(--muted-2)}
.term-body{padding:14px 16px;font-family:var(--mono);font-size:11.5px;line-height:1.6;flex:1;overflow:hidden;position:relative}
.term-line{display:flex;gap:10px;align-items:flex-start;opacity:0;transform:translateY(4px);animation:typeIn .35s ease forwards}
.term-time{color:var(--muted-2);flex:none;width:60px;font-size:10.5px;padding-top:1px}
.term-text{color:#cccddd;flex:1;min-width:0;word-break:break-word}
.term-text .ok{color:var(--success)}
.term-text .warn{color:var(--warning)}
.term-text .err{color:var(--danger)}
.term-text .acc{color:var(--accent)}
.term-text .dim{color:var(--muted)}
.term-text .pkg{color:#c084fc}
.term-prompt{display:inline-flex;align-items:center;gap:7px;color:var(--success)}
.term-prompt::before{content:"$";color:var(--accent);font-weight:600}
@keyframes typeIn{to{opacity:1;transform:translateY(0)}}
.cursor{display:inline-block;width:7px;height:13px;background:var(--accent);vertical-align:-2px;animation:blink 1s steps(1) infinite;margin-left:2px}
@keyframes blink{50%{opacity:0}}
/* mini activity row */
.live-tags{display:flex;flex-wrap:wrap;gap:6px}
.live-tag{display:inline-flex;align-items:center;gap:5px;padding:3px 7px;font-size:10.5px;font-family:var(--mono);color:var(--muted);background:var(--surface);border:1px solid var(--border);border-radius:4px}
.live-tag .dot{width:5px;height:5px;border-radius:50%;background:var(--success)}
.live-tag.warn .dot{background:var(--warning)}
/* responsive */
@media (max-width:1080px){
.page{grid-template-columns:1fr}
.live-side{display:none}
.form-side{padding:28px}
}
@media (max-width:520px){
.form-side{padding:22px}
.sso-row{grid-template-columns:1fr}
.form-footer{justify-content:flex-start}
}
</style>
</head>
<body>
<div class="page">
<!-- ============ LEFT: FORM ============ -->
<section class="form-side">
<div class="top-row">
<div class="brand">
<div class="brand-mark" aria-hidden="true"></div>
<div class="brand-name">CluPilot<span>v2.4</span></div>
</div>
<span class="status-chip"><span class="pulse-dot"></span> All systems operational</span>
</div>
<div class="form-wrap">
<header>
<h1>Willkommen zurück</h1>
<p class="sub">Bei CluPilot anmelden oder <a href="#">neues Konto erstellen</a></p>
</header>
<div class="sso-row">
<button class="sso-btn" type="button">
<svg width="15" height="15" viewBox="0 0 24 24"><path fill="#4285F4" d="M22.5 12.27c0-.79-.07-1.54-.2-2.27H12v4.51h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.32z"/><path fill="#34A853" d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.99.66-2.25 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84A11 11 0 0 0 12 23z"/><path fill="#FBBC05" d="M5.84 14.10A6.6 6.6 0 0 1 5.5 12c0-.73.13-1.44.34-2.10V7.06H2.18A11 11 0 0 0 1 12c0 1.77.42 3.45 1.18 4.94l3.66-2.84z"/><path fill="#EA4335" d="M12 5.38c1.62 0 3.07.56 4.21 1.64l3.15-3.15C17.45 2.10 14.96 1 12 1A11 11 0 0 0 2.18 7.06l3.66 2.84C6.71 7.30 9.14 5.38 12 5.38z"/></svg>
Google
</button>
<button class="sso-btn" type="button">
<svg width="15" height="15" viewBox="0 0 24 24" fill="currentColor"><path d="M12 .5C5.65.5.5 5.65.5 12c0 5.08 3.29 9.39 7.86 10.91.58.11.79-.25.79-.55v-2.02c-3.2.7-3.87-1.36-3.87-1.36-.52-1.33-1.27-1.68-1.27-1.68-1.04-.71.08-.7.08-.7 1.15.08 1.76 1.18 1.76 1.18 1.02 1.75 2.69 1.24 3.35.95.1-.74.4-1.24.73-1.53-2.55-.29-5.24-1.28-5.24-5.69 0-1.26.45-2.28 1.18-3.09-.12-.29-.51-1.46.11-3.05 0 0 .97-.31 3.18 1.18.92-.26 1.91-.39 2.89-.39.98 0 1.97.13 2.89.39 2.20-1.49 3.17-1.18 3.17-1.18.63 1.59.23 2.76.12 3.05.74.81 1.18 1.83 1.18 3.09 0 4.42-2.69 5.40-5.25 5.68.41.36.78 1.06.78 2.14v3.18c0 .31.21.67.79.55C20.21 21.39 23.5 17.08 23.5 12c0-6.35-5.15-11.5-11.5-11.5z"/></svg>
GitHub
</button>
</div>
<div class="divider">oder mit E-Mail</div>
<form id="login-form" autocomplete="on">
<div class="field">
<label class="field-label" for="email">E-Mail-Adresse</label>
<div class="input">
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="5" width="18" height="14" rx="2"/><path d="M3 7l9 6 9-6"/></svg>
<input id="email" name="email" type="email" placeholder="du@firma.de" autocomplete="email" required value="m.weber@acme-cluster.de" />
</div>
</div>
<div class="field">
<label class="field-label" for="password">
Passwort
<a href="#">Vergessen?</a>
</label>
<div class="input">
<svg 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>
<input id="password" name="password" type="password" placeholder="••••••••••••" autocomplete="current-password" required value="hunter2-very-secure" />
<button type="button" class="eye" id="toggle-eye" aria-label="Passwort anzeigen">
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M1 12s4-7 11-7 11 7 11 7-4 7-11 7S1 12 1 12z"/><circle cx="12" cy="12" r="3"/></svg>
</button>
</div>
</div>
<div class="row-between">
<label class="checkbox">
<input type="checkbox" checked />
<span class="box">
<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3.5" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12l5 5L20 7"/></svg>
</span>
<span>30 Tage angemeldet bleiben</span>
</label>
</div>
<button class="btn-primary" type="submit">
Anmelden
<svg class="arrow" 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>
</button>
<div class="alt-options">
<button class="alt-btn" type="button">
<svg class="ico" width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M22 2L11 13"/><path d="M22 2l-7 20-4-9-9-4 20-7z"/></svg>
Magic-Link per E-Mail senden
</button>
<button class="alt-btn" type="button">
<svg class="ico" width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 12c0-4.97-4.03-9-9-9-2.5 0-4.76 1.02-6.39 2.67M3 12c0 4.97 4.03 9 9 9 2.5 0 4.76-1.02 6.39-2.67"/><path d="M9 12a3 3 0 1 0 6 0 3 3 0 1 0-6 0M12 1v4M12 19v4"/></svg>
Mit Passkey anmelden
<span class="badge">empfohlen</span>
</button>
</div>
</form>
</div>
<footer class="form-footer">
<div class="lock">
<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2"><rect x="5" y="11" width="14" height="9" rx="2"/><path d="M8 11V7a4 4 0 0 1 8 0v4"/></svg>
TLS 1.3 · AES-256 · region eu-central-1
</div>
<div class="links">
<a href="#">Datenschutz</a>
<a href="#">AGB</a>
<a href="#">Status</a>
<a href="#">Hilfe</a>
</div>
</footer>
</section>
<!-- ============ RIGHT: LIVE PANEL ============ -->
<aside class="live-side" aria-hidden="true">
<div class="live-head">
<div class="label">— live cluster</div>
<div class="meta">eu-central-1 · build #4f8a32c</div>
</div>
<div>
<p class="hero-quote">Verwalte <em>87 WordPress-Sites</em> auf 6 Servern. Ein Login. Volle Kontrolle.</p>
<p class="hero-sub">SSH-Tunnel, Backups, Updates und Security in einer Konsole — von Push-to-Deploy bis Brute-Force-Defense.</p>
</div>
<div class="stat-strip">
<div class="stat">
<div class="stat-label">Sites Online</div>
<div class="stat-value">87<span class="unit">/87</span></div>
<div class="stat-trend">● 100% Uptime · 24h</div>
</div>
<div class="stat">
<div class="stat-label">Ø Response</div>
<div class="stat-value">142<span class="unit">ms</span></div>
<div class="stat-trend">↓ 8.4% vs. 7d</div>
</div>
<div class="stat">
<div class="stat-label">Blocked / 24h</div>
<div class="stat-value">128</div>
<div class="stat-trend warn">↑ 23% Brute-force</div>
</div>
</div>
<div class="term-card">
<div class="term-head">
<div class="term-dots"><span></span><span></span><span></span></div>
<div class="term-title">ssh <a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="e28d9291a28797cf92908d86cfd2d3cc818e97928b8e8d96cc8b8d">[email&#160;protected]</a></div>
<div class="term-meta">pts/0</div>
</div>
<div class="term-body" id="term-body">
<!-- lines injected by JS -->
</div>
</div>
<div class="live-tags">
<span class="live-tag"><span class="dot"></span>eu-prod-01 · 23 sites</span>
<span class="live-tag"><span class="dot"></span>eu-prod-02 · 19 sites</span>
<span class="live-tag"><span class="dot"></span>edge-ams · 14 sites</span>
<span class="live-tag warn"><span class="dot"></span>us-east-01 · CPU 78%</span>
<span class="live-tag"><span class="dot"></span>staging · 6 sites</span>
</div>
</aside>
</div>
<script data-cfasync="false" src="/cdn-cgi/scripts/5c5dd728/cloudflare-static/email-decode.min.js"></script><script>
/* ----- password eye toggle ----- */
const eyeBtn = document.getElementById('toggle-eye');
const pwd = document.getElementById('password');
eyeBtn.addEventListener('click', () => {
pwd.type = pwd.type === 'password' ? 'text' : 'password';
});
/* ----- prevent submit (demo) ----- */
document.getElementById('login-form').addEventListener('submit', (e) => {
e.preventDefault();
const btn = e.target.querySelector('.btn-primary');
const orig = btn.innerHTML;
btn.innerHTML = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" style="animation:spin 0.8s linear infinite"><path d="M21 12a9 9 0 1 1-6.2-8.55"/></svg> Authentifiziere…';
btn.disabled = true;
setTimeout(() => { btn.innerHTML = orig; btn.disabled = false; }, 1800);
});
/* ----- fake terminal stream ----- */
const lines = [
['09:42:18', '<span class="term-prompt"></span> <span class="dim">clupilot</span> status --cluster=acme'],
['09:42:18', '<span class="ok"></span> 87/87 sites <span class="dim">healthy</span> · <span class="acc">142ms</span> ø response'],
['09:42:19', '<span class="ok"></span> backup eu-prod-02 → <span class="pkg">s3://clupilot-backups</span> <span class="dim">248 MB</span>'],
['09:42:21', '<span class="acc"></span> deploying <span class="pkg">Yoast-SEO@22.5</span> to <span class="dim">3 sites…</span>'],
['09:42:23', '<span class="ok"></span> agentur-helio.com <span class="dim">(312ms)</span>'],
['09:42:24', '<span class="ok"></span> blog.fintech-radar.io <span class="dim">(287ms)</span>'],
['09:42:24', '<span class="ok"></span> docs.opencrate.dev <span class="dim">(298ms)</span>'],
['09:42:26', '<span class="warn">!</span> classic.tonhoff-bau.de <span class="dim">PHP 7.4 EOL — migration queued</span>'],
['09:42:28', '<span class="err"></span> fw <span class="dim">blocked</span> 17 login attempts from <span class="acc">185.220.101.18</span> <span class="dim">(TOR)</span>'],
['09:42:30', '<span class="ok"></span> SSL renewed: <span class="pkg">*.kunde-mueller.de</span> <span class="dim">until 2026-08-25</span>'],
['09:42:32', '<span class="term-prompt"></span> <span class="dim">tail -f /var/log/clupilot/audit.log</span><span class="cursor"></span>'],
];
const body = document.getElementById('term-body');
let i = 0;
function addLine(){
if(i >= lines.length){
// restart cycle after a pause
setTimeout(() => { body.innerHTML = ''; i = 0; addLine(); }, 5000);
return;
}
const [t, html] = lines[i++];
const el = document.createElement('div');
el.className = 'term-line';
el.innerHTML = '<div class="term-time">'+t+'</div><div class="term-text">'+html+'</div>';
body.appendChild(el);
// auto-scroll
body.scrollTop = body.scrollHeight;
// remove top lines if too many
while(body.children.length > 11) body.removeChild(body.firstChild);
setTimeout(addLine, 600 + Math.random()*450);
}
addLine();
/* spinner keyframes injected */
const style = document.createElement('style');
style.textContent = '@keyframes spin{to{transform:rotate(360deg)}}';
document.head.appendChild(style);
</script>
</body>
</html>

View File

@ -0,0 +1,477 @@
<!doctype html>
<html lang="de">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>Konto erstellen · CluPilot</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet">
<style>
:root{
--bg:#0a0a0f;
--bg-2:#0c0c12;
--card:#16161e;
--card-hover:#1a1a24;
--surface:#101019;
--surface-2:#1d1d28;
--border:rgba(255,255,255,0.06);
--border-strong:rgba(255,255,255,0.10);
--border-focus:rgba(79,142,247,0.5);
--fg:#f0f0f5;
--muted:#8888a0;
--muted-2:#5c5c70;
--accent:#4f8ef7;
--accent-2:#7c5cff;
--accent-soft:rgba(79,142,247,0.12);
--success:#3ecf8e;
--warning:#f5a623;
--danger:#f05252;
--success-soft:rgba(62,207,142,0.12);
--mono:'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, monospace;
}
*{box-sizing:border-box}
html,body{margin:0;padding:0;background:var(--bg);color:var(--fg);font-family:'Inter',system-ui,-apple-system,sans-serif;font-size:13.5px;line-height:1.5;-webkit-font-smoothing:antialiased;min-height:100vh}
a{color:var(--accent);text-decoration:none}
a:hover{text-decoration:underline}
button{font-family:inherit;color:inherit;background:none;border:0;cursor:pointer}
body::before{
content:"";position:fixed;inset:0;z-index:0;pointer-events:none;
background:
radial-gradient(60% 50% at 12% 10%, rgba(79,142,247,0.10), transparent 65%),
radial-gradient(50% 50% at 88% 100%, rgba(124,92,255,0.08), transparent 65%);
}
body::after{
content:"";position:fixed;inset:0;z-index:0;pointer-events:none;
background-image:
linear-gradient(rgba(255,255,255,0.018) 1px, transparent 1px),
linear-gradient(90deg, rgba(255,255,255,0.018) 1px, transparent 1px);
background-size:32px 32px;
mask-image:radial-gradient(ellipse at center, #000 30%, transparent 75%);
}
.page{position:relative;z-index:1;min-height:100vh;display:grid;grid-template-columns:minmax(440px,1fr) 1fr;gap:0}
/* ===== left: form ===== */
.form-side{display:flex;flex-direction:column;padding:32px 48px;min-height:100vh}
.top-row{display:flex;align-items:center;justify-content:space-between;gap:16px}
.brand{display:flex;align-items:center;gap:10px}
.brand-mark{width:30px;height:30px;border-radius:8px;background:linear-gradient(135deg,#4f8ef7 0%,#7c5cff 100%);position:relative;display:grid;place-items:center;box-shadow:0 0 0 1px rgba(255,255,255,0.06) inset, 0 6px 16px rgba(79,142,247,0.30)}
.brand-mark::before{content:"";width:11px;height:11px;border-radius:50%;background:#0a0a0f;box-shadow:inset 0 0 0 2px #fff}
.brand-name{font-weight:600;font-size:15px;letter-spacing:-0.01em}
.brand-name span{color:var(--muted);font-weight:500;margin-left:5px;font-size:11.5px}
.top-link{font-size:12.5px;color:var(--muted)}
.top-link a{margin-left:6px;font-weight:500}
/* Stepper */
.stepper{display:flex;align-items:center;gap:10px;margin-top:32px;font-family:var(--mono);font-size:11px;color:var(--muted-2)}
.step{display:flex;align-items:center;gap:7px}
.step-num{width:20px;height:20px;border-radius:50%;border:1px solid var(--border);display:grid;place-items:center;font-size:10.5px;color:var(--muted);background:var(--surface)}
.step.active .step-num{border-color:var(--accent);color:var(--accent);background:var(--accent-soft)}
.step.done .step-num{border-color:var(--success);color:#0a0a0f;background:var(--success)}
.step-label{font-size:11px;color:var(--muted);text-transform:uppercase;letter-spacing:0.06em;font-weight:500}
.step.active .step-label{color:var(--fg)}
.step-line{flex:1;height:1px;background:var(--border);min-width:14px;max-width:28px}
.form-wrap{margin:auto 0;display:flex;flex-direction:column;gap:22px;max-width:420px;width:100%;padding:32px 0}
h1{margin:0;font-size:24px;font-weight:600;letter-spacing:-0.02em;line-height:1.2}
.sub{margin:6px 0 0;font-size:13.5px;color:var(--muted)}
.sub a{font-weight:500}
/* SSO row */
.sso-row{display:grid;grid-template-columns:1fr 1fr;gap:8px}
.sso-btn{display:flex;align-items:center;justify-content:center;gap:8px;padding:9px 12px;background:var(--surface);border:1px solid var(--border);border-radius:8px;color:var(--fg);font-size:12.5px;font-weight:500;transition:all .15s}
.sso-btn:hover{background:var(--surface-2);border-color:var(--border-strong)}
.divider{display:flex;align-items:center;gap:12px;color:var(--muted-2);font-size:11px;font-family:var(--mono);text-transform:uppercase;letter-spacing:0.08em}
.divider::before,.divider::after{content:"";flex:1;height:1px;background:var(--border)}
/* Inputs */
form{display:flex;flex-direction:column;gap:14px}
.field{display:flex;flex-direction:column;gap:6px}
.field-row{display:grid;grid-template-columns:1fr 1fr;gap:10px}
.field-label{display:flex;align-items:center;justify-content:space-between;font-size:11.5px;color:var(--muted);font-weight:500}
.field-label .hint{font-family:var(--mono);font-size:10.5px;color:var(--muted-2);font-weight:400}
.input{display:flex;align-items:center;gap:10px;padding:0 12px;background:var(--surface);border:1px solid var(--border);border-radius:8px;transition:border-color .15s, background .15s, box-shadow .15s;height:40px}
.input:hover{border-color:var(--border-strong)}
.input:focus-within{border-color:var(--border-focus);background:#12121c;box-shadow:0 0 0 3px rgba(79,142,247,0.10)}
.input svg{color:var(--muted-2);flex:none}
.input input{flex:1;background:none;border:0;outline:0;color:var(--fg);font-size:13.5px;font-family:inherit;min-width:0;height:100%}
.input input::placeholder{color:var(--muted-2)}
.input.with-suffix{padding-right:0}
.input-suffix{height:100%;display:flex;align-items:center;padding:0 12px;border-left:1px solid var(--border);color:var(--muted);font-family:var(--mono);font-size:11.5px;background:rgba(255,255,255,0.015);border-top-right-radius:8px;border-bottom-right-radius:8px}
.input .eye{color:var(--muted);padding:4px;border-radius:4px;display:grid;place-items:center;transition:color .15s,background .15s;margin-right:4px}
.input .eye:hover{color:var(--fg);background:var(--surface-2)}
.input-check{color:var(--success);margin-right:8px;display:none}
.input.valid .input-check{display:inline-flex}
/* Slug preview */
.slug-preview{font-family:var(--mono);font-size:11px;color:var(--muted);margin-top:2px}
.slug-preview b{color:var(--accent);font-weight:500}
/* Password strength */
.pw-meter{display:flex;gap:4px;margin-top:6px}
.pw-meter span{flex:1;height:3px;border-radius:2px;background:var(--surface-2);transition:background .2s}
.pw-meter.s1 span:nth-child(-n+1){background:var(--danger)}
.pw-meter.s2 span:nth-child(-n+2){background:var(--warning)}
.pw-meter.s3 span:nth-child(-n+3){background:var(--accent)}
.pw-meter.s4 span{background:var(--success)}
.pw-rules{display:flex;flex-wrap:wrap;gap:6px 12px;margin-top:6px;font-family:var(--mono);font-size:10.5px;color:var(--muted-2)}
.pw-rule{display:inline-flex;align-items:center;gap:5px}
.pw-rule .bullet{width:5px;height:5px;border-radius:50%;background:var(--muted-2);transition:background .15s}
.pw-rule.ok{color:var(--success)}
.pw-rule.ok .bullet{background:var(--success)}
/* Checkbox + submit */
.checkbox{display:flex;align-items:flex-start;gap:10px;font-size:12.5px;color:var(--muted);cursor:pointer;user-select:none;line-height:1.45}
.checkbox input{display:none}
.checkbox .box{width:15px;height:15px;border-radius:4px;border:1px solid var(--border-strong);background:var(--surface);display:grid;place-items:center;transition:all .15s;flex:none;margin-top:1px}
.checkbox input:checked + .box{background:var(--accent);border-color:var(--accent)}
.checkbox input:checked + .box svg{opacity:1;transform:scale(1)}
.checkbox .box svg{opacity:0;transform:scale(0.6);transition:all .15s;color:#fff}
.checkbox:hover{color:var(--fg)}
.btn-primary{display:inline-flex;align-items:center;justify-content:center;gap:8px;padding:10px 16px;background:var(--accent);color:#fff;border-radius:8px;font-weight:500;font-size:13.5px;height:40px;transition:background .15s,transform .05s;box-shadow:0 1px 0 rgba(255,255,255,0.08) inset, 0 1px 3px rgba(79,142,247,0.35)}
.btn-primary:hover{background:#5e98f7}
.btn-primary:active{transform:translateY(1px)}
.btn-primary .arrow{transition:transform .15s}
.btn-primary:hover .arrow{transform:translateX(2px)}
/* footer */
.form-footer{display:flex;align-items:center;justify-content:space-between;gap:12px;color:var(--muted);font-size:11.5px;font-family:var(--mono);padding-top:12px;border-top:1px solid var(--border);flex-wrap:wrap}
.form-footer .links{display:flex;gap:14px}
.form-footer .links a{color:var(--muted)}
.form-footer .links a:hover{color:var(--fg);text-decoration:none}
.form-footer .lock{display:flex;align-items:center;gap:6px}
/* ===== right: benefits panel ===== */
.live-side{position:relative;border-left:1px solid var(--border);background:linear-gradient(180deg,#0c0c12 0%, #0a0a0f 100%);padding:36px 40px;overflow:hidden;display:flex;flex-direction:column;gap:22px;min-height:100vh}
.live-side::before{
content:"";position:absolute;inset:0;pointer-events:none;
background:radial-gradient(70% 50% at 100% 0%, rgba(79,142,247,0.12), transparent 65%);
}
.live-side > *{position:relative}
.trial-card{background:var(--card);border:1px solid var(--border);border-radius:12px;padding:18px 20px;position:relative;overflow:hidden}
.trial-card::before{content:"";position:absolute;top:0;left:0;right:0;height:2px;background:linear-gradient(90deg,#4f8ef7,#7c5cff)}
.trial-row{display:flex;align-items:center;justify-content:space-between}
.trial-tag{font-family:var(--mono);font-size:10.5px;color:var(--accent);background:var(--accent-soft);padding:3px 8px;border-radius:5px;border:1px solid rgba(79,142,247,0.25);font-weight:500;text-transform:uppercase;letter-spacing:0.05em}
.trial-count{font-family:var(--mono);font-size:11px;color:var(--muted);display:inline-flex;align-items:center;gap:6px}
.pulse-dot{width:6px;height:6px;border-radius:50%;background:var(--success);position:relative;flex:none}
.pulse-dot::after{content:"";position:absolute;inset:-3px;border-radius:50%;background:currentColor;color:var(--success);opacity:0.4;animation:pulse 1.8s ease-out infinite}
@keyframes pulse{0%{transform:scale(0.6);opacity:.5}100%{transform:scale(2.4);opacity:0}}
.trial-title{font-size:22px;font-weight:600;letter-spacing:-0.015em;line-height:1.25;margin-top:12px;max-width:420px}
.trial-title em{font-style:normal;background:linear-gradient(90deg,#4f8ef7,#a78bfa);-webkit-background-clip:text;background-clip:text;color:transparent;font-weight:700}
.trial-sub{color:var(--muted);font-size:13px;margin-top:6px;max-width:440px}
.benefit-list{list-style:none;padding:0;margin:0;display:flex;flex-direction:column;gap:14px}
.benefit{display:flex;gap:12px;align-items:flex-start}
.benefit-ico{width:30px;height:30px;border-radius:8px;background:var(--accent-soft);border:1px solid rgba(79,142,247,0.22);color:var(--accent);display:grid;place-items:center;flex:none}
.benefit-ico.success{background:var(--success-soft);border-color:rgba(62,207,142,0.22);color:var(--success)}
.benefit-ico.purple{background:rgba(124,92,255,0.12);border-color:rgba(124,92,255,0.25);color:#a78bfa}
.benefit-ico.warn{background:rgba(245,166,35,0.10);border-color:rgba(245,166,35,0.22);color:var(--warning)}
.benefit-body{flex:1;min-width:0}
.benefit-title{font-size:13.5px;font-weight:500;color:var(--fg)}
.benefit-desc{font-size:12.5px;color:var(--muted);margin-top:2px;line-height:1.5}
/* Quote */
.quote-card{margin-top:auto;background:var(--card);border:1px solid var(--border);border-radius:12px;padding:18px 20px}
.quote-text{font-size:13.5px;color:#dcdce8;line-height:1.55;font-weight:400}
.quote-text::before{content:"„";color:var(--accent);font-size:24px;line-height:0;margin-right:4px;vertical-align:-6px;font-weight:600}
.quote-text::after{content:""";color:var(--accent);font-weight:600}
.quote-author{display:flex;align-items:center;gap:10px;margin-top:12px}
.avatar{width:32px;height:32px;border-radius:50%;background:linear-gradient(135deg,#3ecf8e,#2a9d6f);display:grid;place-items:center;color:#0a0a0f;font-weight:600;font-size:12px;font-family:var(--mono)}
.avatar.alt{background:linear-gradient(135deg,#7c5cff,#4f8ef7);color:#fff}
.author-meta{font-size:12px}
.author-name{color:var(--fg);font-weight:500}
.author-role{color:var(--muted);font-size:11px;font-family:var(--mono)}
/* Logo strip */
.logos{display:flex;align-items:center;gap:20px;flex-wrap:wrap;opacity:0.55;font-family:var(--mono);font-size:11px;color:var(--muted);text-transform:uppercase;letter-spacing:0.08em}
.logo-pill{padding:5px 10px;border:1px solid var(--border);border-radius:5px;background:var(--surface)}
/* responsive */
@media (max-width:1080px){
.page{grid-template-columns:1fr}
.live-side{display:none}
.form-side{padding:28px}
}
@media (max-width:520px){
.form-side{padding:22px}
.sso-row{grid-template-columns:1fr}
.field-row{grid-template-columns:1fr}
.form-footer{justify-content:flex-start}
.stepper .step-label{display:none}
}
</style>
</head>
<body>
<div class="page">
<!-- ============ LEFT: FORM ============ -->
<section class="form-side">
<div class="top-row">
<div class="brand">
<div class="brand-mark" aria-hidden="true"></div>
<div class="brand-name">CluPilot<span>v2.4</span></div>
</div>
<div class="top-link">Schon Konto?<a href="#">Anmelden →</a></div>
</div>
<div class="stepper" aria-label="Registrierungs-Schritte">
<div class="step active">
<div class="step-num">1</div>
<div class="step-label">Konto</div>
</div>
<div class="step-line"></div>
<div class="step">
<div class="step-num">2</div>
<div class="step-label">Workspace</div>
</div>
<div class="step-line"></div>
<div class="step">
<div class="step-num">3</div>
<div class="step-label">Erster Server</div>
</div>
</div>
<div class="form-wrap">
<header>
<h1>14 Tage kostenlos testen</h1>
<p class="sub">Keine Kreditkarte. Volle Funktionen. Jederzeit kündbar.</p>
</header>
<div class="sso-row">
<button class="sso-btn" type="button">
<svg width="15" height="15" viewBox="0 0 24 24"><path fill="#4285F4" d="M22.5 12.27c0-.79-.07-1.54-.2-2.27H12v4.51h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.32z"/><path fill="#34A853" d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.99.66-2.25 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84A11 11 0 0 0 12 23z"/><path fill="#FBBC05" d="M5.84 14.10A6.6 6.6 0 0 1 5.5 12c0-.73.13-1.44.34-2.10V7.06H2.18A11 11 0 0 0 1 12c0 1.77.42 3.45 1.18 4.94l3.66-2.84z"/><path fill="#EA4335" d="M12 5.38c1.62 0 3.07.56 4.21 1.64l3.15-3.15C17.45 2.10 14.96 1 12 1A11 11 0 0 0 2.18 7.06l3.66 2.84C6.71 7.30 9.14 5.38 12 5.38z"/></svg>
Mit Google fortfahren
</button>
<button class="sso-btn" type="button">
<svg width="15" height="15" viewBox="0 0 24 24" fill="currentColor"><path d="M12 .5C5.65.5.5 5.65.5 12c0 5.08 3.29 9.39 7.86 10.91.58.11.79-.25.79-.55v-2.02c-3.2.7-3.87-1.36-3.87-1.36-.52-1.33-1.27-1.68-1.27-1.68-1.04-.71.08-.7.08-.7 1.15.08 1.76 1.18 1.76 1.18 1.02 1.75 2.69 1.24 3.35.95.1-.74.4-1.24.73-1.53-2.55-.29-5.24-1.28-5.24-5.69 0-1.26.45-2.28 1.18-3.09-.12-.29-.51-1.46.11-3.05 0 0 .97-.31 3.18 1.18.92-.26 1.91-.39 2.89-.39.98 0 1.97.13 2.89.39 2.20-1.49 3.17-1.18 3.17-1.18.63 1.59.23 2.76.12 3.05.74.81 1.18 1.83 1.18 3.09 0 4.42-2.69 5.40-5.25 5.68.41.36.78 1.06.78 2.14v3.18c0 .31.21.67.79.55C20.21 21.39 23.5 17.08 23.5 12c0-6.35-5.15-11.5-11.5-11.5z"/></svg>
Mit GitHub fortfahren
</button>
</div>
<div class="divider">oder mit E-Mail</div>
<form id="signup-form" autocomplete="on">
<div class="field-row">
<div class="field">
<label class="field-label" for="firstname">Vorname</label>
<div class="input">
<input id="firstname" type="text" placeholder="Marie" autocomplete="given-name" required />
</div>
</div>
<div class="field">
<label class="field-label" for="lastname">Nachname</label>
<div class="input">
<input id="lastname" type="text" placeholder="Weber" autocomplete="family-name" required />
</div>
</div>
</div>
<div class="field">
<label class="field-label" for="email">Arbeits-E-Mail</label>
<div class="input" id="email-wrap">
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="5" width="18" height="14" rx="2"/><path d="M3 7l9 6 9-6"/></svg>
<input id="email" type="email" placeholder="du@firma.de" autocomplete="email" required />
<svg class="input-check" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="M5 12l5 5L20 7"/></svg>
</div>
</div>
<div class="field">
<label class="field-label" for="workspace">
Workspace-Name
<span class="hint">URL für dein Team</span>
</label>
<div class="input with-suffix">
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="4" width="18" height="7" rx="1.5"/><rect x="3" y="13" width="18" height="7" rx="1.5"/><circle cx="7" cy="7.5" r="0.7" fill="currentColor"/><circle cx="7" cy="16.5" r="0.7" fill="currentColor"/></svg>
<input id="workspace" type="text" placeholder="Acme Cluster" required />
<span class="input-suffix" id="slug-suffix">.clupilot.io</span>
</div>
<div class="slug-preview">Erreichbar unter <b id="slug-preview">dein-team</b>.clupilot.io</div>
</div>
<div class="field">
<label class="field-label" for="password">
Passwort
<span class="hint">min. 10 Zeichen</span>
</label>
<div class="input">
<svg 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>
<input id="password" type="password" placeholder="••••••••••••" autocomplete="new-password" required />
<button type="button" class="eye" id="toggle-eye" aria-label="Passwort anzeigen">
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M1 12s4-7 11-7 11 7 11 7-4 7-11 7S1 12 1 12z"/><circle cx="12" cy="12" r="3"/></svg>
</button>
</div>
<div class="pw-meter" id="pw-meter"><span></span><span></span><span></span><span></span></div>
<div class="pw-rules">
<span class="pw-rule" data-rule="len"><span class="bullet"></span>10+ Zeichen</span>
<span class="pw-rule" data-rule="upper"><span class="bullet"></span>Großbuchstabe</span>
<span class="pw-rule" data-rule="num"><span class="bullet"></span>Zahl</span>
<span class="pw-rule" data-rule="sym"><span class="bullet"></span>Sonderzeichen</span>
</div>
</div>
<label class="checkbox">
<input type="checkbox" id="agree" required />
<span class="box">
<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3.5" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12l5 5L20 7"/></svg>
</span>
<span>Ich akzeptiere die <a href="#">AGB</a> und stimme der <a href="#">Datenschutzerklärung</a> zu. CluPilot speichert SSH-Keys verschlüsselt in der EU.</span>
</label>
<button class="btn-primary" type="submit">
Konto erstellen
<svg class="arrow" 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>
</button>
</form>
</div>
<footer class="form-footer">
<div class="lock">
<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2"><rect x="5" y="11" width="14" height="9" rx="2"/><path d="M8 11V7a4 4 0 0 1 8 0v4"/></svg>
TLS 1.3 · AES-256 · region eu-central-1
</div>
<div class="links">
<a href="#">Datenschutz</a>
<a href="#">AGB</a>
<a href="#">Status</a>
<a href="#">Hilfe</a>
</div>
</footer>
</section>
<!-- ============ RIGHT: TRIAL / BENEFITS ============ -->
<aside class="live-side" aria-hidden="true">
<div class="trial-card">
<div class="trial-row">
<span class="trial-tag">14 Tage Pro · gratis</span>
<span class="trial-count"><span class="pulse-dot"></span> 47 Teams diese Woche</span>
</div>
<div class="trial-title">In <em>60 Sekunden</em> deinen ersten Server verbunden.</div>
<div class="trial-sub">SSH-Schlüssel hochladen, Site importieren, fertig. Wir kümmern uns ums Monitoring, Backups und Updates — du um dein Business.</div>
</div>
<ul class="benefit-list">
<li class="benefit">
<div class="benefit-ico">
<svg 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>
</div>
<div class="benefit-body">
<div class="benefit-title">SSH-First Setup</div>
<div class="benefit-desc">Verbinde Hetzner, Hostinger oder deinen eigenen Server in 2 Klicks — keine Agents, kein Custom-DNS.</div>
</div>
</li>
<li class="benefit">
<div class="benefit-ico success">
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 12c0 5-9 9-9 9s-9-4-9-9V5l9-3 9 3v7z"/><path d="M9 11l2 2 4-4"/></svg>
</div>
<div class="benefit-body">
<div class="benefit-title">Stündliche Backups inklusive</div>
<div class="benefit-desc">Verschlüsselt nach Frankfurt-S3, 30 Tage Retention, One-Click-Restore — auch für die WP-Datenbank.</div>
</div>
</li>
<li class="benefit">
<div class="benefit-ico purple">
<svg width="15" height="15" 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>
</div>
<div class="benefit-body">
<div class="benefit-title">Bulk-Updates ohne Drama</div>
<div class="benefit-desc">WordPress, Plugins und Themes auf 100 Sites gleichzeitig — mit Staging-Diff und automatischem Rollback bei Fehler.</div>
</div>
</li>
<li class="benefit">
<div class="benefit-ico warn">
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 3l8 4v5c0 5-3.5 8-8 9-4.5-1-8-4-8-9V7l8-4z"/><path d="M12 8v5M12 16h.01"/></svg>
</div>
<div class="benefit-body">
<div class="benefit-title">Brute-Force-Defense aktiv</div>
<div class="benefit-desc">Geo-Fencing, Rate-Limits und WP-User-Lockout — schon im Trial dabei. Letzte 24h: 12.488 Versuche blockiert.</div>
</div>
</li>
</ul>
<div class="quote-card">
<div class="quote-text">Statt 18 SSH-Sessions in Terminal-Tabs habe ich jetzt eine Konsole. Mein Agency-Team hostet 87 Sites — und schläft endlich wieder durch.</div>
<div class="quote-author">
<div class="avatar">JK</div>
<div class="author-meta">
<div class="author-name">Julian Koch</div>
<div class="author-role">CTO · Helio Digital · 87 Sites</div>
</div>
</div>
</div>
<div class="logos">
<span>vertrauen uns</span>
<span class="logo-pill">helio.</span>
<span class="logo-pill">acme▲</span>
<span class="logo-pill">opencrate</span>
<span class="logo-pill">bergmann</span>
<span class="logo-pill">+ 2.1k</span>
</div>
</aside>
</div>
<script>
/* ---- workspace slug preview ---- */
const ws = document.getElementById('workspace');
const slug = document.getElementById('slug-preview');
const slugify = s => s.toLowerCase().trim().replace(/[^a-z0-9]+/g,'-').replace(/^-|-$/g,'').slice(0,32) || 'dein-team';
ws.addEventListener('input', () => { slug.textContent = slugify(ws.value); });
/* ---- email valid checkmark ---- */
const email = document.getElementById('email');
const emailWrap = document.getElementById('email-wrap');
email.addEventListener('input', () => {
emailWrap.classList.toggle('valid', /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email.value));
});
/* ---- pw strength + rules ---- */
const pwd = document.getElementById('password');
const meter = document.getElementById('pw-meter');
const rules = document.querySelectorAll('.pw-rule');
pwd.addEventListener('input', () => {
const v = pwd.value;
const tests = {
len: v.length >= 10,
upper: /[A-Z]/.test(v),
num: /[0-9]/.test(v),
sym: /[^A-Za-z0-9]/.test(v),
};
let score = 0;
Object.keys(tests).forEach(k => {
const ok = tests[k];
const el = document.querySelector(`.pw-rule[data-rule="${k}"]`);
el.classList.toggle('ok', ok);
if(ok) score++;
});
meter.className = 'pw-meter' + (v.length ? ' s'+score : '');
});
/* ---- eye toggle ---- */
document.getElementById('toggle-eye').addEventListener('click', () => {
pwd.type = pwd.type === 'password' ? 'text' : 'password';
});
/* ---- submit (demo) ---- */
document.getElementById('signup-form').addEventListener('submit', (e) => {
e.preventDefault();
const btn = e.target.querySelector('.btn-primary');
const orig = btn.innerHTML;
btn.innerHTML = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" style="animation:spin 0.8s linear infinite"><path d="M21 12a9 9 0 1 1-6.2-8.55"/></svg> Konto wird angelegt…';
btn.disabled = true;
setTimeout(() => { btn.innerHTML = orig; btn.disabled = false; }, 1800);
});
const style = document.createElement('style');
style.textContent = '@keyframes spin{to{transform:rotate(360deg)}}';
document.head.appendChild(style);
</script>
</body>
</html>