Feature: API-Key/Webhook responsive div-grid, Sandbox-Icon in Domains, Search-fix
- API-Key-Tabelle: unified CSS-Grid div-layout (kein separates mobile/desktop HTML mehr), Scopes auf max. 2 Badges + +N Modal, Lösch-Bestätigung via Livewire-Modal - Webhook-Tabelle: selbes div-grid Pattern, Status/HTTP inline auf Mobile - Globale Suche: go()-Methode fixed (forceClose + setTimeout 350ms gegen resetState-Race) - Domains: Sandbox-Icon ersetzt Globus durch gelbes Warndreieck wenn Sandbox aktiv - Sandbox: SandboxRoute-Model, SandboxService, Migration, Routen-Verwaltung - CSS: mw-kl-*/mw-whl-* Grid-Klassen, minmax(0,1fr) Fix für Text-Truncation Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>main v1.1.161
parent
fc8dbf894a
commit
38d22c85ed
|
|
@ -0,0 +1,102 @@
|
|||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
## Commands
|
||||
|
||||
```bash
|
||||
# Development (starts PHP server + queue + logs + Vite concurrently)
|
||||
composer dev
|
||||
|
||||
# Build assets for production
|
||||
npm run build
|
||||
|
||||
# Vite dev server only
|
||||
npm run dev
|
||||
|
||||
# Run all tests
|
||||
composer test
|
||||
php artisan test
|
||||
|
||||
# Run a single test file
|
||||
php artisan test tests/Feature/ExampleTest.php
|
||||
|
||||
# Run a single test by name
|
||||
php artisan test --filter=TestName
|
||||
|
||||
# Migrations
|
||||
php artisan migrate
|
||||
```
|
||||
|
||||
## Architecture
|
||||
|
||||
### Design
|
||||
- Tailwind CSS v4
|
||||
- Modals sind immer vom Livewire Modals
|
||||
- Kein Inline-Style nur wenn wirklich notwendig.
|
||||
- Immer prüfen das das Style nicht kaputt ist und wenn Icons in Buttons mit Text sind, sind diese immer nebeneinadner Nie übereinander.
|
||||
|
||||
### What this is
|
||||
Mailwolt is a self-hosted mail server administration panel (German UI). It manages domains, mailboxes, aliases, DNS records (DKIM/DMARC/SPF/TLSA), TLS, Fail2ban, IMAP, and email quarantine/queues. It also has a mail sandbox for testing Postfix transports.
|
||||
|
||||
### Layout & Routing
|
||||
- **`resources/views/layouts/dvx.blade.php`** is the actual app layout (not `app.blade.php`). Livewire full-page components use `#[Layout('layouts.dvx')]`.
|
||||
- Routes are in `routes/web.php` — each page maps directly to a Livewire full-page component via `->name()`.
|
||||
- Navigation structure is driven by `config/ui-menu.php`.
|
||||
- Für den Style wird Tailwind CSS v4
|
||||
|
||||
### Livewire component structure
|
||||
```
|
||||
app/Livewire/Ui/
|
||||
├── Nx/ — Current production UI (Dashboard, DomainList, MailboxList, AliasList, etc.)
|
||||
├── Domain/ — Domain modals and DKIM/DNS views
|
||||
├── Mail/ — Mailbox/alias modals, queue, quarantine
|
||||
├── Security/ — Fail2ban, SSL, RSpamd, TLS, audit logs
|
||||
├── System/ — Settings, users, API keys, webhooks, sandbox, backups
|
||||
├── Search/ — Global search palette modal
|
||||
└── Webmail/ — Webmail-specific components
|
||||
```
|
||||
|
||||
Full-page components live in `Ui/Nx/` and `Ui/System/`, `Ui/Security/`, etc. Modals are always in a `Modal/` subfolder and extend `LivewireUI\Modal\ModalComponent`.
|
||||
|
||||
### Modals (wire-elements/modal v2)
|
||||
- Open from **outside** a Livewire component: `onclick="Livewire.dispatch('openModal', {component:'ui.system.modal.my-modal', arguments:{key:value}})"`
|
||||
- Open from **inside** a Livewire component: `$this->dispatch('openModal', component: '...', arguments: [...])`
|
||||
- **Never** use `wire:click="$dispatch('openModal',...)"` outside a Livewire component context — it won't work.
|
||||
- Modal argument keys must match the `mount(int $keyName)` parameter names exactly.
|
||||
- To prevent closing on backdrop/Escape, override in the modal class:
|
||||
```php
|
||||
public static function closeModalOnClickAway(): bool { return false; }
|
||||
public static function closeModalOnEscape(): bool { return false; }
|
||||
public static function closeModalOnEscapeIsForceful(): bool { return false; }
|
||||
```
|
||||
- To force-close the entire modal stack: `$this->forceClose()->closeModal()`
|
||||
|
||||
### Livewire dependency injection
|
||||
- **Never** use constructor injection in Livewire components — Livewire calls `new Component()` with no args.
|
||||
- Use `boot(MyService $service)` instead: this is called on every request and supports DI.
|
||||
|
||||
### CSS design system
|
||||
The app uses a custom `mw-*` variable and class system defined in `resources/css/app.css` (Tailwind CSS v4, no `tailwind.config.js`):
|
||||
|
||||
**CSS variables:**
|
||||
- `--mw-bg`, `--mw-bg3`, `--mw-bg4` — background layers
|
||||
- `--mw-b1`, `--mw-b2`, `--mw-b3` — border shades
|
||||
- `--mw-t1` through `--mw-t5` — text shades (t1 = primary, t4/t5 = muted)
|
||||
- `--mw-v`, `--mw-v2`, `--mw-vbg` — purple accent (primary brand color)
|
||||
- `--mw-gr` — green (success)
|
||||
|
||||
**Reusable component classes:** `.mw-btn-primary`, `.mw-btn-secondary`, `.mw-btn-cancel`, `.mw-btn-save`, `.mw-btn-del`, `.mbx-act-btn`, `.mbx-act-danger`, `.mw-modal-frame`, `.mw-modal-head`, `.mw-modal-body`, `.mw-modal-foot`, `.mw-modal-label`, `.mw-modal-input`, `.mw-modal-error`, `.mbx-badge-mute`, `.mbx-badge-ok`, `.mbx-badge-warn`.
|
||||
|
||||
### Services
|
||||
`app/Services/` contains: `DkimService`, `DnsRecordService`, `ImapService`, `MailStorage`, `SandboxMailParser`, `SandboxService`, `TlsaService`, `TotpService`, `WebhookService`.
|
||||
|
||||
### API
|
||||
REST API under `/api/v1/` uses Laravel Sanctum. Token abilities map to scopes like `mailboxes:read`, `domains:write`, etc. Defined in `routes/api.php`.
|
||||
|
||||
### Sandbox mail system
|
||||
The mail sandbox intercepts Postfix mail via a pipe transport (`php artisan sandbox:receive`). `SandboxRoute` model controls which domains/addresses are intercepted. `SandboxService::syncTransportFile()` writes `/etc/postfix/transport.sandbox` and runs `postmap`.
|
||||
|
||||
### Queue & real-time
|
||||
- Queue driver: database (configurable). Jobs in `app/Jobs/`.
|
||||
- Real-time updates use Laravel Reverb + Pusher.js. Livewire polls (`wire:poll.5s`) are used as fallback on some pages.
|
||||
|
|
@ -6,6 +6,7 @@ use App\Models\BackupJob;
|
|||
use App\Models\BackupPolicy;
|
||||
use App\Models\Domain;
|
||||
use App\Models\MailUser;
|
||||
use App\Models\SandboxRoute;
|
||||
use App\Models\Setting as SettingModel;
|
||||
use App\Support\CacheVer;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
|
|
@ -43,7 +44,8 @@ class Dashboard extends Component
|
|||
'mailboxCount' => MailUser::where('is_system', false)->where('is_active', true)->count(),
|
||||
'servicesActive' => $servicesActive,
|
||||
'servicesTotal' => count($services),
|
||||
'alertCount' => 0,
|
||||
'alertCount' => SandboxRoute::where('is_active', true)->count(),
|
||||
'sandboxAlerts' => SandboxRoute::activeRoutes(),
|
||||
'backup' => $this->backupData(),
|
||||
'mailHostname' => gethostname() ?: 'mailserver',
|
||||
'services' => $services,
|
||||
|
|
|
|||
|
|
@ -126,21 +126,20 @@ class SearchPaletteModal extends ModalComponent
|
|||
|
||||
public function go(string $type, int $id): void
|
||||
{
|
||||
// Schließe die Palette …
|
||||
$this->dispatch('closeModal');
|
||||
|
||||
// … und navigiere / öffne Kontext:
|
||||
// - Domain → scrolle/markiere Domainkarte
|
||||
// - Mailbox → öffne Bearbeiten-Modal
|
||||
// Passe an, was du bevorzugst:
|
||||
if ($type === 'domain') {
|
||||
$this->dispatch('focus:domain', id: $id);
|
||||
$component = 'ui.domain.modal.domain-edit-modal';
|
||||
$arguments = ['domainId' => $id];
|
||||
} elseif ($type === 'mailbox') {
|
||||
// direkt Edit-Modal auf
|
||||
$this->dispatch('openModal', component:'ui.mail.modal.mailbox-edit-modal', arguments: [$id]);
|
||||
} elseif ($type === 'user') {
|
||||
$this->dispatch('focus:user', id: $id);
|
||||
$component = 'ui.mail.modal.mailbox-edit-modal';
|
||||
$arguments = ['mailboxId' => $id];
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
// Search palette schließen, dann nach dem State-Reset (300 ms) das Ziel-Modal öffnen
|
||||
$this->forceClose()->closeModal();
|
||||
$payload = json_encode(['component' => $component, 'arguments' => $arguments]);
|
||||
$this->js("setTimeout(()=>Livewire.dispatch('openModal',{$payload}),350)");
|
||||
}
|
||||
|
||||
public static function modalMaxWidth(): string
|
||||
|
|
|
|||
|
|
@ -20,21 +20,22 @@ class ApiKeyCreateModal extends ModalComponent
|
|||
'domains:write' => 'Domains schreiben',
|
||||
];
|
||||
|
||||
public static function closeModalOnClickAway(): bool { return false; }
|
||||
public static function closeModalOnEscape(): bool { return false; }
|
||||
public static function closeModalOnEscapeIsForceful(): bool { return false; }
|
||||
|
||||
public function create(): void
|
||||
{
|
||||
$this->validate([
|
||||
'name' => 'required|string|max:80',
|
||||
'selected' => 'required|array|min:1',
|
||||
'name' => 'required|string|max:80',
|
||||
'selected' => 'required|array|min:1',
|
||||
'selected.*' => 'in:' . implode(',', array_keys(self::$availableScopes)),
|
||||
], [
|
||||
'selected.required' => 'Bitte mindestens einen Scope auswählen.',
|
||||
'selected.min' => 'Bitte mindestens einen Scope auswählen.',
|
||||
]);
|
||||
|
||||
$token = Auth::user()->createToken(
|
||||
$this->name,
|
||||
$this->selected,
|
||||
);
|
||||
$token = Auth::user()->createToken($this->name, $this->selected);
|
||||
|
||||
$pat = $token->accessToken;
|
||||
if ($this->sandbox) {
|
||||
|
|
@ -43,7 +44,10 @@ class ApiKeyCreateModal extends ModalComponent
|
|||
}
|
||||
|
||||
$this->dispatch('token-created', plainText: $token->plainTextToken);
|
||||
$this->closeModal();
|
||||
$this->dispatch('openModal',
|
||||
component: 'ui.system.modal.api-key-show-modal',
|
||||
arguments: ['plainText' => $token->plainTextToken],
|
||||
);
|
||||
}
|
||||
|
||||
public function toggleAll(): void
|
||||
|
|
|
|||
|
|
@ -0,0 +1,42 @@
|
|||
<?php
|
||||
|
||||
namespace App\Livewire\Ui\System\Modal;
|
||||
|
||||
use App\Models\PersonalAccessToken;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use LivewireUI\Modal\ModalComponent;
|
||||
|
||||
class ApiKeyDeleteModal extends ModalComponent
|
||||
{
|
||||
public int $tokenId;
|
||||
public string $tokenName = '';
|
||||
|
||||
public function mount(int $tokenId): void
|
||||
{
|
||||
$token = PersonalAccessToken::where('tokenable_id', Auth::id())
|
||||
->where('tokenable_type', Auth::user()::class)
|
||||
->findOrFail($tokenId);
|
||||
|
||||
$this->tokenId = $tokenId;
|
||||
$this->tokenName = $token->name;
|
||||
}
|
||||
|
||||
public function delete(): void
|
||||
{
|
||||
PersonalAccessToken::where('tokenable_id', Auth::id())
|
||||
->where('tokenable_type', Auth::user()::class)
|
||||
->findOrFail($this->tokenId)
|
||||
->delete();
|
||||
|
||||
$this->dispatch('toast', type: 'done', badge: 'API Key',
|
||||
title: 'Gelöscht', text: "Key <b>{$this->tokenName}</b> wurde entfernt.", duration: 4000);
|
||||
|
||||
$this->dispatch('token-deleted');
|
||||
$this->closeModal();
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.ui.system.modal.api-key-delete-modal');
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
<?php
|
||||
|
||||
namespace App\Livewire\Ui\System\Modal;
|
||||
|
||||
use App\Models\PersonalAccessToken;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use LivewireUI\Modal\ModalComponent;
|
||||
|
||||
class ApiKeyScopesModal extends ModalComponent
|
||||
{
|
||||
public string $tokenName = '';
|
||||
public array $scopes = [];
|
||||
|
||||
public function mount(int $tokenId): void
|
||||
{
|
||||
$token = PersonalAccessToken::where('tokenable_id', Auth::id())
|
||||
->where('tokenable_type', Auth::user()::class)
|
||||
->findOrFail($tokenId);
|
||||
|
||||
$this->tokenName = $token->name;
|
||||
$this->scopes = $token->abilities;
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.ui.system.modal.api-key-scopes-modal');
|
||||
}
|
||||
}
|
||||
|
|
@ -13,9 +13,18 @@ class ApiKeyShowModal extends ModalComponent
|
|||
$this->plainText = $plainText;
|
||||
}
|
||||
|
||||
public static function closeModalOnClickAway(): bool { return false; }
|
||||
public static function closeModalOnEscape(): bool { return false; }
|
||||
public static function closeModalOnEscapeIsForceful(): bool { return false; }
|
||||
|
||||
public function dismiss(): void
|
||||
{
|
||||
$this->forceClose()->closeModal();
|
||||
}
|
||||
|
||||
public static function modalMaxWidth(): string
|
||||
{
|
||||
return 'md';
|
||||
return '2xl';
|
||||
}
|
||||
|
||||
public function render()
|
||||
|
|
|
|||
|
|
@ -0,0 +1,68 @@
|
|||
<?php
|
||||
|
||||
namespace App\Livewire\Ui\System;
|
||||
|
||||
use App\Models\SandboxRoute;
|
||||
use App\Services\SandboxService;
|
||||
use Livewire\Attributes\Computed;
|
||||
use Livewire\Component;
|
||||
|
||||
class SandboxRoutes extends Component
|
||||
{
|
||||
public string $newType = 'domain';
|
||||
public string $newTarget = '';
|
||||
public ?string $syncMessage = null;
|
||||
public bool $syncOk = false;
|
||||
|
||||
public function boot(SandboxService $sandboxService): void
|
||||
{
|
||||
$this->sandboxService = $sandboxService;
|
||||
}
|
||||
|
||||
public function addRoute(): void
|
||||
{
|
||||
if ($this->newType !== 'global') {
|
||||
$this->validate(['newTarget' => 'required|string|max:255']);
|
||||
}
|
||||
|
||||
$target = $this->newType === 'global' ? null : trim($this->newTarget);
|
||||
$this->sandboxService->enable($this->newType, $target);
|
||||
$this->sandboxService->syncTransportFile();
|
||||
$this->dispatch('sandbox-route-changed');
|
||||
$this->newTarget = '';
|
||||
}
|
||||
|
||||
public function removeRoute(int $id): void
|
||||
{
|
||||
$this->sandboxService->delete($id);
|
||||
$this->sandboxService->syncTransportFile();
|
||||
$this->dispatch('sandbox-route-changed');
|
||||
}
|
||||
|
||||
public function toggleRoute(int $id): void
|
||||
{
|
||||
$route = SandboxRoute::findOrFail($id);
|
||||
$route->is_active = !$route->is_active;
|
||||
$route->save();
|
||||
$this->sandboxService->syncTransportFile();
|
||||
$this->dispatch('sandbox-route-changed');
|
||||
}
|
||||
|
||||
public function syncPostfix(): void
|
||||
{
|
||||
$result = $this->sandboxService->syncTransportFile();
|
||||
$this->syncOk = $result['ok'];
|
||||
$this->syncMessage = $result['message'];
|
||||
}
|
||||
|
||||
#[Computed]
|
||||
public function routes()
|
||||
{
|
||||
return SandboxRoute::orderBy('type')->orderBy('target')->get();
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.ui.system.sandbox-routes');
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,63 @@
|
|||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Collection;
|
||||
|
||||
class SandboxRoute extends Model
|
||||
{
|
||||
protected $fillable = ['type', 'target', 'is_active'];
|
||||
|
||||
protected $casts = [
|
||||
'is_active' => 'boolean',
|
||||
];
|
||||
|
||||
/**
|
||||
* All active routes ordered by type and target.
|
||||
*/
|
||||
public static function activeRoutes(): Collection
|
||||
{
|
||||
return static::where('is_active', true)
|
||||
->orderBy('type')
|
||||
->orderBy('target')
|
||||
->get();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether sandbox is active for a given domain.
|
||||
* True if global sandbox is active OR domain-specific rule is active.
|
||||
*/
|
||||
public static function isActiveForDomain(string $domain): bool
|
||||
{
|
||||
return static::where('is_active', true)
|
||||
->where(function ($q) use ($domain) {
|
||||
$q->where('type', 'global')
|
||||
->orWhere(function ($q2) use ($domain) {
|
||||
$q2->where('type', 'domain')->where('target', $domain);
|
||||
});
|
||||
})
|
||||
->exists();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether sandbox is active for a given address.
|
||||
* True if global is active OR domain matches OR address-specific rule exists.
|
||||
*/
|
||||
public static function isActiveForAddress(string $address): bool
|
||||
{
|
||||
$domain = substr(strrchr($address, '@'), 1) ?: '';
|
||||
|
||||
return static::where('is_active', true)
|
||||
->where(function ($q) use ($address, $domain) {
|
||||
$q->where('type', 'global')
|
||||
->orWhere(function ($q2) use ($domain) {
|
||||
$q2->where('type', 'domain')->where('target', $domain);
|
||||
})
|
||||
->orWhere(function ($q3) use ($address) {
|
||||
$q3->where('type', 'address')->where('target', $address);
|
||||
});
|
||||
})
|
||||
->exists();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,88 @@
|
|||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\SandboxRoute;
|
||||
|
||||
class SandboxService
|
||||
{
|
||||
/**
|
||||
* Enable (or create) a sandbox route.
|
||||
*/
|
||||
public function enable(string $type, ?string $target): SandboxRoute
|
||||
{
|
||||
return SandboxRoute::updateOrCreate(
|
||||
['type' => $type, 'target' => $target],
|
||||
['is_active' => true]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Disable a sandbox route by id.
|
||||
*/
|
||||
public function disable(int $id): void
|
||||
{
|
||||
SandboxRoute::findOrFail($id)->update(['is_active' => false]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a sandbox route by id.
|
||||
*/
|
||||
public function delete(int $id): void
|
||||
{
|
||||
SandboxRoute::findOrFail($id)->delete();
|
||||
}
|
||||
|
||||
/**
|
||||
* Write /etc/postfix/transport.sandbox and run postmap.
|
||||
* Returns ['ok' => bool, 'message' => string].
|
||||
*/
|
||||
public function syncTransportFile(): array
|
||||
{
|
||||
$file = config('sandbox.transport_file', '/etc/postfix/transport.sandbox');
|
||||
|
||||
$routes = SandboxRoute::where('is_active', true)
|
||||
->orderBy('type')
|
||||
->orderBy('target')
|
||||
->get();
|
||||
|
||||
$lines = [];
|
||||
$hasGlobal = false;
|
||||
|
||||
foreach ($routes as $route) {
|
||||
if ($route->type === 'global') {
|
||||
$hasGlobal = true;
|
||||
} elseif ($route->type === 'domain') {
|
||||
$lines[] = $route->target . ' sandbox:';
|
||||
} elseif ($route->type === 'address') {
|
||||
$lines[] = $route->target . ' sandbox:';
|
||||
}
|
||||
}
|
||||
|
||||
// Global catch-all goes at the end
|
||||
if ($hasGlobal) {
|
||||
$lines[] = '* sandbox:';
|
||||
}
|
||||
|
||||
$content = implode("\n", $lines);
|
||||
if ($lines) {
|
||||
$content .= "\n";
|
||||
}
|
||||
|
||||
try {
|
||||
file_put_contents($file, $content);
|
||||
} catch (\Throwable $e) {
|
||||
return ['ok' => false, 'message' => 'Fehler beim Schreiben: ' . $e->getMessage()];
|
||||
}
|
||||
|
||||
$out = [];
|
||||
$code = 0;
|
||||
exec('postmap ' . escapeshellarg($file) . ' 2>&1', $out, $code);
|
||||
|
||||
if ($code !== 0) {
|
||||
return ['ok' => false, 'message' => 'postmap Fehler: ' . implode(' ', $out)];
|
||||
}
|
||||
|
||||
return ['ok' => true, 'message' => 'Transport-Datei synchronisiert (' . count($lines) . ' Einträge).'];
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('sandbox_routes', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->enum('type', ['global', 'domain', 'address'])->default('domain');
|
||||
$table->string('target')->nullable(); // domain or email, null = global
|
||||
$table->boolean('is_active')->default(true);
|
||||
$table->timestamps();
|
||||
$table->unique(['type', 'target']);
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('sandbox_routes');
|
||||
}
|
||||
};
|
||||
|
|
@ -5,6 +5,7 @@
|
|||
@import "../fonts/Space/font.css";
|
||||
|
||||
@source '../../vendor/laravel/framework/src/Illuminate/Pagination/resources/views/*.blade.php';
|
||||
@source '../../vendor/wire-elements/modal/resources/views/*.blade.php';
|
||||
@source '../../storage/framework/views/*.php';
|
||||
@source '../**/*.blade.php';
|
||||
@source '../**/*.js';
|
||||
|
|
@ -99,6 +100,19 @@
|
|||
.mw-divider {
|
||||
@apply border-t border-white/10 my-4;
|
||||
}
|
||||
|
||||
/* ── Header utility classes ── */
|
||||
.btn-ghost {
|
||||
@apply inline-flex items-center justify-center rounded-lg
|
||||
text-white/60 hover:text-white hover:bg-white/5
|
||||
transition-colors duration-150 cursor-pointer;
|
||||
padding: 0.4rem;
|
||||
}
|
||||
|
||||
.popup {
|
||||
@apply rounded-xl border border-white/10 shadow-xl z-50
|
||||
bg-[#0f172a]/95 backdrop-blur-xl text-white/80;
|
||||
}
|
||||
}
|
||||
|
||||
/* ============ BOOT-STATE: keine Sprünge ============ */
|
||||
|
|
@ -1093,40 +1107,125 @@ select {
|
|||
/* ── Main ── */
|
||||
.mw-main { flex: 1; display: flex; flex-direction: column; overflow: hidden; min-width: 0; }
|
||||
|
||||
/* ── Topbar ── */
|
||||
/* ════════════════════════════════════════
|
||||
Topbar — Redesign
|
||||
════════════════════════════════════════ */
|
||||
.mw-topbar {
|
||||
height: 50px; padding: 0 22px; border-bottom: 1px solid var(--mw-b1);
|
||||
background: var(--mw-bg); display: flex; align-items: center;
|
||||
justify-content: space-between; flex-shrink: 0;
|
||||
height: 54px;
|
||||
padding: 0 20px;
|
||||
border-bottom: 1px solid var(--mw-b1);
|
||||
background: var(--mw-bg);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
flex-shrink: 0;
|
||||
gap: 8px;
|
||||
}
|
||||
.mw-breadcrumb { display: flex; align-items: center; gap: 5px; font-size: 13px; color: var(--mw-t4); }
|
||||
.mw-breadcrumb b { color: var(--mw-t2); font-weight: 500; }
|
||||
.mw-topbar-right { display: flex; align-items: center; gap: 8px; }
|
||||
|
||||
.mw-live { display: flex; align-items: center; gap: 5px; font-size: 11.5px; color: var(--mw-t3); }
|
||||
.mw-live-dot {
|
||||
width: 6px; height: 6px; border-radius: 50%; background: var(--mw-gr);
|
||||
box-shadow: 0 0 5px rgba(34,197,94,.5); animation: mw-pulse 2s infinite;
|
||||
/* ── Links ── */
|
||||
.mw-tb-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
}
|
||||
@keyframes mw-pulse { 0%,100%{opacity:1} 50%{opacity:.3} }
|
||||
|
||||
.mw-search {
|
||||
display: flex; align-items: center; gap: 6px; background: var(--mw-bg3);
|
||||
border: 1px solid var(--mw-b1); border-radius: 6px; padding: 5px 10px;
|
||||
font-size: 12px; color: var(--mw-t4); cursor: pointer;
|
||||
.mw-hamburger {
|
||||
display: none;
|
||||
flex-shrink: 0;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 32px; height: 32px;
|
||||
border-radius: 8px;
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--mw-t3);
|
||||
cursor: pointer;
|
||||
transition: background .15s, color .15s;
|
||||
}
|
||||
.mw-search kbd { font-size: 9.5px; opacity: .5; margin-left: 2px; }
|
||||
.mw-ip {
|
||||
.mw-hamburger:hover { background: var(--mw-bg3); color: var(--mw-t1); }
|
||||
|
||||
.mw-tb-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 13px;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
.mw-tb-parent { color: var(--mw-t4); white-space: nowrap; flex-shrink: 0; }
|
||||
.mw-tb-sep { color: var(--mw-b3); flex-shrink: 0; }
|
||||
.mw-tb-current { color: var(--mw-t2); font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||
|
||||
/* ── Rechts ── */
|
||||
.mw-tb-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* Live-Dot */
|
||||
.mw-tb-live {
|
||||
display: flex; align-items: center; gap: 5px;
|
||||
padding: 4px 9px;
|
||||
border-radius: 99px;
|
||||
background: rgba(34,197,94,.08);
|
||||
border: 1px solid rgba(34,197,94,.18);
|
||||
font-size: 11px; font-weight: 500; color: #4ade80;
|
||||
}
|
||||
.mw-tb-live-dot {
|
||||
width: 6px; height: 6px; border-radius: 50%;
|
||||
background: #22c55e;
|
||||
box-shadow: 0 0 6px rgba(34,197,94,.7);
|
||||
animation: mw-pulse 2s infinite;
|
||||
}
|
||||
@keyframes mw-pulse { 0%,100%{opacity:1} 50%{opacity:.35} }
|
||||
|
||||
/* Suchbutton */
|
||||
.mw-tb-search {
|
||||
display: flex; align-items: center; gap: 7px;
|
||||
height: 32px; padding: 0 12px;
|
||||
background: var(--mw-bg3);
|
||||
border: 1px solid var(--mw-b2);
|
||||
border-radius: 8px;
|
||||
font-size: 12px; color: var(--mw-t4);
|
||||
cursor: pointer;
|
||||
transition: border-color .15s, color .15s;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.mw-tb-search:hover { border-color: var(--mw-b3); color: var(--mw-t2); }
|
||||
.mw-tb-kbd {
|
||||
display: inline-flex; align-items: center; justify-content: center;
|
||||
padding: 2px 5px;
|
||||
background: var(--mw-b1); border: 1px solid var(--mw-b2);
|
||||
border-radius: 4px; font-size: 10px; color: var(--mw-t5);
|
||||
font-family: ui-monospace, monospace;
|
||||
}
|
||||
|
||||
/* Hostname-Badge */
|
||||
.mw-tb-host {
|
||||
display: flex; align-items: center; gap: 5px;
|
||||
height: 32px; padding: 0 10px;
|
||||
background: var(--mw-bg3); border: 1px solid var(--mw-b2);
|
||||
border-radius: 8px;
|
||||
font-family: ui-monospace, monospace; font-size: 11px; color: var(--mw-t4);
|
||||
background: var(--mw-bg3); border: 1px solid var(--mw-b1); border-radius: 5px; padding: 4px 9px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.mw-btn-primary {
|
||||
display: inline-flex; align-items: center; gap: 5px;
|
||||
background: var(--mw-v); border: none; border-radius: 6px; padding: 6px 14px;
|
||||
font-size: 12.5px; font-weight: 600; color: #fff; cursor: pointer;
|
||||
box-shadow: 0 0 10px rgba(124,58,237,.35);
|
||||
|
||||
/* + Domain Button */
|
||||
.mw-tb-add {
|
||||
display: flex; align-items: center; gap: 6px;
|
||||
height: 32px; padding: 0 14px;
|
||||
background: var(--mw-v);
|
||||
border: none; border-radius: 8px;
|
||||
font-size: 12.5px; font-weight: 600; color: #fff;
|
||||
cursor: pointer; flex-shrink: 0;
|
||||
box-shadow: 0 2px 12px rgba(124,58,237,.35);
|
||||
transition: background .15s, box-shadow .15s;
|
||||
}
|
||||
.mw-btn-primary:hover { background: #6d28d9; }
|
||||
.mw-tb-add:hover { background: #6d28d9; box-shadow: 0 2px 16px rgba(124,58,237,.5); }
|
||||
|
||||
/* ── Content ── */
|
||||
.mw-content { flex: 1; overflow-y: auto; padding: 20px 22px 30px; }
|
||||
|
|
@ -1346,13 +1445,16 @@ select {
|
|||
/* ── Responsive ── */
|
||||
@media (max-width: 1024px) {
|
||||
.mw-content { padding: 16px 18px 24px; }
|
||||
.mw-topbar { padding: 0 18px; }
|
||||
.mw-topbar { padding: 0 16px; }
|
||||
}
|
||||
@media (max-width: 900px) {
|
||||
.mw-bottom-grid { grid-template-columns: 1fr; }
|
||||
.mw-right-col { flex-direction: row; }
|
||||
/* Hostname auf Tablet kompakter */
|
||||
.mw-tb-host { font-size: 10.5px; padding: 0 8px; }
|
||||
}
|
||||
@media (max-width: 768px) {
|
||||
/* Sidebar off-canvas */
|
||||
.mw-sidebar {
|
||||
position: fixed; left: 0; top: 0; z-index: 50;
|
||||
transform: translateX(-100%); transition: transform .25s;
|
||||
|
|
@ -1363,10 +1465,10 @@ select {
|
|||
background: rgba(0,0,0,.55); z-index: 40;
|
||||
}
|
||||
.mw-sidebar-overlay.open { display: block; }
|
||||
.mw-hamburger {
|
||||
display: flex !important; background: none; border: none;
|
||||
color: var(--mw-t3); cursor: pointer; padding: 4px; margin-right: 4px;
|
||||
}
|
||||
/* Hamburger einblenden */
|
||||
.mw-hamburger { display: flex; }
|
||||
/* Parent bleibt sichtbar (3 Buchstaben) */
|
||||
/* Layout */
|
||||
.mw-metric-grid { grid-template-columns: repeat(2, 1fr); }
|
||||
.mw-status-grid { grid-template-columns: repeat(2, 1fr); }
|
||||
.mw-shell { display: block; height: 100vh; }
|
||||
|
|
@ -1376,7 +1478,176 @@ select {
|
|||
@media (max-width: 480px) {
|
||||
.mw-metric-grid { grid-template-columns: 1fr; }
|
||||
.mw-status-grid { grid-template-columns: 1fr; }
|
||||
.mw-ip { display: none; }
|
||||
.mw-topbar { padding: 0 12px; gap: 5px; }
|
||||
.mw-tb-parent { display: none; }
|
||||
.mw-tb-sep { display: none; }
|
||||
.mw-tb-search-text { display: none; }
|
||||
.mw-tb-kbd { display: none; }
|
||||
.mw-tb-search { padding: 0 9px; }
|
||||
.mw-tb-add-text { display: none; }
|
||||
.mw-tb-add { padding: 0 9px; }
|
||||
}
|
||||
|
||||
/* ── API Key Layout ── */
|
||||
.mw-apikey-layout {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 340px;
|
||||
gap: 14px;
|
||||
align-items: start;
|
||||
}
|
||||
.mw-apikey-layout > * { min-width: 0; }
|
||||
@media (max-width: 900px) {
|
||||
.mw-apikey-layout { grid-template-columns: 1fr; }
|
||||
}
|
||||
/* ── Responsive list helpers ── */
|
||||
@media (max-width: 640px) {
|
||||
.mw-col-hide-sm { display: none !important; }
|
||||
.mw-only-desktop { display: none !important; }
|
||||
}
|
||||
@media (min-width: 641px) {
|
||||
.mw-only-mobile { display: none !important; }
|
||||
}
|
||||
|
||||
/* ── API Key div-list ── */
|
||||
.mw-kl-head,
|
||||
.mw-kl-row {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) 104px 80px 90px 40px;
|
||||
gap: 8px;
|
||||
padding: 8px 14px;
|
||||
align-items: center;
|
||||
min-width: 0;
|
||||
}
|
||||
.mw-kl-head {
|
||||
font-size: 9.5px; font-weight: 600; text-transform: uppercase;
|
||||
letter-spacing: .5px; color: var(--mw-t4);
|
||||
border-bottom: 1px solid var(--mw-b1);
|
||||
}
|
||||
.mw-kl-row {
|
||||
border-bottom: 1px solid var(--mw-b1);
|
||||
transition: background .1s;
|
||||
}
|
||||
.mw-kl-row:last-child { border-bottom: none; }
|
||||
.mw-kl-row:hover { background: rgba(255,255,255,.015); }
|
||||
.mw-kl-row > * { min-width: 0; overflow: hidden; }
|
||||
.mw-kl-date-inline { display: none; }
|
||||
.mw-kl-head span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
|
||||
@media (max-width: 900px) and (min-width: 641px) {
|
||||
.mw-kl-head,
|
||||
.mw-kl-row { grid-template-columns: minmax(0, 1fr) 120px 80px 40px; }
|
||||
.mw-kl-date { display: none; }
|
||||
.mw-kl-date-inline { display: inline; }
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.mw-kl-head { display: none; }
|
||||
.mw-kl-row {
|
||||
grid-template-columns: 1fr auto;
|
||||
grid-template-rows: auto auto;
|
||||
gap: 5px 8px;
|
||||
padding: 10px 12px;
|
||||
}
|
||||
.mw-kl-name { grid-column: 1; grid-row: 1; }
|
||||
.mw-kl-scopes { grid-column: 1; grid-row: 2; flex-wrap: wrap; overflow: visible; }
|
||||
.mw-kl-modus { grid-column: 2; grid-row: 1; }
|
||||
.mw-kl-date { display: none; }
|
||||
.mw-kl-date-inline { display: inline; }
|
||||
.mw-kl-actions { grid-column: 2; grid-row: 2; justify-self: end; }
|
||||
}
|
||||
|
||||
/* ── Webhook div-list ── */
|
||||
.mw-whl-head,
|
||||
.mw-whl-row {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 124px 72px 54px 82px;
|
||||
gap: 8px;
|
||||
padding: 8px 14px;
|
||||
align-items: center;
|
||||
min-width: 0;
|
||||
}
|
||||
.mw-whl-head {
|
||||
font-size: 9.5px; font-weight: 600; text-transform: uppercase;
|
||||
letter-spacing: .5px; color: var(--mw-t4);
|
||||
border-bottom: 1px solid var(--mw-b1);
|
||||
}
|
||||
.mw-whl-row {
|
||||
border-bottom: 1px solid var(--mw-b1);
|
||||
transition: background .1s;
|
||||
}
|
||||
.mw-whl-row:last-child { border-bottom: none; }
|
||||
.mw-whl-row:hover { background: rgba(255,255,255,.015); }
|
||||
.mw-whl-row > * { min-width: 0; }
|
||||
.mw-whl-status-sm, .mw-whl-http-sm { display: none; }
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.mw-whl-head { display: none; }
|
||||
.mw-whl-row {
|
||||
grid-template-columns: 1fr auto;
|
||||
grid-template-rows: auto auto;
|
||||
gap: 5px 8px;
|
||||
padding: 10px 12px;
|
||||
}
|
||||
.mw-whl-webhook { grid-column: 1; grid-row: 1; }
|
||||
.mw-whl-events { grid-column: 1; grid-row: 2; }
|
||||
.mw-whl-status { display: none; }
|
||||
.mw-whl-http { display: none; }
|
||||
.mw-whl-date { display: none; }
|
||||
.mw-whl-actions {
|
||||
grid-column: 2; grid-row: 1 / 3;
|
||||
display: flex; flex-direction: column; gap: 4px; align-self: start;
|
||||
}
|
||||
.mw-whl-status-sm, .mw-whl-http-sm { display: inline; }
|
||||
}
|
||||
|
||||
/* ── Webhook-Tabelle Layout ── */
|
||||
.mw-wh-layout {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 300px;
|
||||
gap: 14px;
|
||||
align-items: start;
|
||||
}
|
||||
.mw-wh-layout > * { min-width: 0; }
|
||||
@media (max-width: 900px) {
|
||||
.mw-wh-layout { grid-template-columns: 1fr; }
|
||||
}
|
||||
|
||||
/* ── Sandbox Layout ── */
|
||||
.mw-sandbox-client {
|
||||
display: grid;
|
||||
grid-template-columns: 300px 1fr;
|
||||
gap: 0;
|
||||
border: 1px solid var(--mw-b1);
|
||||
border-radius: 10px;
|
||||
overflow: hidden;
|
||||
background: var(--mw-bg2);
|
||||
min-height: 520px;
|
||||
}
|
||||
.mw-sandbox-list {
|
||||
border-right: 1px solid var(--mw-b1);
|
||||
overflow-y: auto;
|
||||
max-height: 680px;
|
||||
}
|
||||
.mw-sandbox-detail {
|
||||
overflow-y: auto;
|
||||
max-height: 680px;
|
||||
min-height: 400px;
|
||||
}
|
||||
.mw-postfix-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr 1fr;
|
||||
gap: 16px;
|
||||
padding: 16px 18px;
|
||||
}
|
||||
@media (max-width: 900px) {
|
||||
.mw-sandbox-client { grid-template-columns: 1fr; }
|
||||
.mw-sandbox-list { border-right: none; border-bottom: 1px solid var(--mw-b1); max-height: 260px; }
|
||||
.mw-sandbox-detail { max-height: none; }
|
||||
.mw-postfix-grid { grid-template-columns: 1fr; }
|
||||
}
|
||||
@media (max-width: 640px) {
|
||||
.mw-sandbox-client { min-height: 0; }
|
||||
.mw-postfix-grid { grid-template-columns: 1fr; }
|
||||
}
|
||||
|
||||
.mw-svc-status-off { color: var(--mw-rd) !important; opacity: 1; }
|
||||
|
|
@ -1743,6 +2014,20 @@ textarea.mw-modal-input { height: auto; line-height: 1.55; padding: 8px 10px; }
|
|||
}
|
||||
|
||||
/* Buttons */
|
||||
.mw-btn-primary {
|
||||
display: inline-flex; align-items: center; gap: 6px;
|
||||
padding: 6px 16px; border-radius: 7px; font-size: 13px; font-weight: 500; cursor: pointer;
|
||||
background: var(--mw-v); border: 1px solid var(--mw-v); color: #fff;
|
||||
box-shadow: 0 2px 8px rgba(124,58,237,.3);
|
||||
}
|
||||
.mw-btn-primary:hover { background: #6d28d9; border-color: #6d28d9; }
|
||||
.mw-btn-secondary {
|
||||
display: inline-flex; align-items: center; gap: 6px;
|
||||
padding: 6px 14px; border-radius: 7px; font-size: 13px; font-weight: 500; cursor: pointer;
|
||||
background: var(--mw-bg3); border: 1px solid var(--mw-b2); color: var(--mw-t2);
|
||||
transition: background .15s, border-color .15s;
|
||||
}
|
||||
.mw-btn-secondary:hover { background: var(--mw-bg4); border-color: var(--mw-b3); color: var(--mw-t1); }
|
||||
.mw-btn-cancel {
|
||||
display: inline-flex; align-items: center; gap: 5px;
|
||||
padding: 6px 14px; border-radius: 7px; font-size: 13px; cursor: pointer;
|
||||
|
|
|
|||
|
|
@ -1,107 +1,119 @@
|
|||
{{--resources/views/components/partials/header.blade.php--}}
|
||||
<div
|
||||
class="#sticky top-0 w-full border-b hr #rounded-lg max-w-9xl mx-auto">
|
||||
<div class="w-full border-b hr">
|
||||
<header id="header" class="header w-full rounded-r-2xl rounded-l-none">
|
||||
<nav class="flex h-[71px] #h-[74px] items-center justify-between #p-3">
|
||||
<div class="#flex-1 md:w-3/5 w-full">
|
||||
<div class="relative flex items-center gap-5">
|
||||
<button class="sidebar-toggle translate-0 right-5 block s#m:hidden text-white/60 hover:text-white text-2xl">
|
||||
<i class="ph ph-list"></i>
|
||||
</button>
|
||||
<div class="flex items-center gap-3">
|
||||
<h1 class="font-bold text-2xl">@yield('header_title')</h1>
|
||||
</div>
|
||||
</div>
|
||||
<nav class="flex h-[71px] items-center justify-between gap-3 px-4">
|
||||
|
||||
{{-- Links: Toggle + Seitentitel --}}
|
||||
<div class="flex min-w-0 flex-1 items-center gap-3">
|
||||
<button class="sidebar-toggle shrink-0 text-white/60 hover:text-white text-2xl">
|
||||
<i class="ph ph-list"></i>
|
||||
</button>
|
||||
<h1 class="truncate font-bold text-xl sm:text-2xl">@yield('header_title')</h1>
|
||||
</div>
|
||||
<div class="flex items-center justify-end gap-2 w-full max-w-96">
|
||||
|
||||
{{-- Rechts: Suche + Aktionen --}}
|
||||
<div class="flex shrink-0 items-center gap-1.5 sm:gap-2">
|
||||
|
||||
{{-- Suchbutton --}}
|
||||
<button
|
||||
id="openSearchPaletteBtn"
|
||||
type="button"
|
||||
class="inline-flex items-center gap-2 rounded-full border border-white/10 bg-white/5 px-3 py-1.5
|
||||
text-white/75 hover:text-white hover:border-white/20"
|
||||
onclick="Livewire.dispatch('openModal',{component:'ui.search.modal.search-palette-modal'})"
|
||||
class="inline-flex items-center gap-2 rounded-full border border-white/10 bg-white/5 px-2.5 py-1.5 text-white/75 hover:text-white hover:border-white/20 sm:px-3"
|
||||
title="Suche öffnen"
|
||||
aria-label="Suche öffnen"
|
||||
wire:click="$dispatch('openModal',{component:'ui.search.modal.search-palette-modal'})"
|
||||
>
|
||||
<i class="ph ph-magnifying-glass text-[16px]"></i>
|
||||
<i class="ph ph-magnifying-glass text-base"></i>
|
||||
<span id="searchShortcutKbd"
|
||||
class="rounded-md bg-white/5 border border-white/10 px-1.5 py-0.5 text-[11px] leading-none">
|
||||
<!-- wird per JS gesetzt -->
|
||||
class="hidden sm:inline-block rounded-md bg-white/5 border border-white/10 px-1.5 py-0.5 text-[11px] leading-none">
|
||||
</span>
|
||||
</button>
|
||||
<button onclick="Livewire.dispatch('openModal',{component:'ui.domain.modal.domain-create-modal'})">
|
||||
|
||||
{{-- Domain erstellen --}}
|
||||
<button
|
||||
type="button"
|
||||
onclick="Livewire.dispatch('openModal',{component:'ui.domain.modal.domain-create-modal'})"
|
||||
class="btn-ghost text-lg !p-2"
|
||||
title="Domain erstellen"
|
||||
>
|
||||
<i class="ph-duotone ph-globe-hemisphere-east"></i>
|
||||
</button>
|
||||
|
||||
@foreach($header as $section)
|
||||
<div class="#modal-button #relative #mr-2">
|
||||
<div class="#p-1.5">
|
||||
@if(!$section['image'])
|
||||
<div x-data="{ openMessages: false }" class="relative flex items-center">
|
||||
<button @click="openMessages = !openMessages" class="btn-ghost text-base !p-2"><i class="ph-duotone ph-bell #mr-1"></i></button>
|
||||
<div
|
||||
x-show="openMessages"
|
||||
@click.away="openMessages = false"
|
||||
x-cloak
|
||||
class="popup absolute top-16 right-0 w-48"
|
||||
x-transition:enter="transition ease-out duration-100"
|
||||
x-transition:enter-start="opacity-0 transform scale-95"
|
||||
x-transition:enter-end="opacity-100 transform scale-100"
|
||||
x-transition:leave="transition ease-in duration-75"
|
||||
x-transition:leave-start="opacity-100 transform scale-100"
|
||||
x-transition:leave-end="opacity-0 transform scale-95"
|
||||
>
|
||||
<div class="flex items-center justify-center h-40">
|
||||
Keine Nachrichten
|
||||
</div>
|
||||
</div>
|
||||
@if(!$section['image'])
|
||||
{{-- Benachrichtigungen --}}
|
||||
<div x-data="{ openMessages: false }" class="relative flex items-center">
|
||||
<button
|
||||
@click="openMessages = !openMessages"
|
||||
class="btn-ghost text-base !p-2"
|
||||
title="Benachrichtigungen"
|
||||
>
|
||||
<i class="ph-duotone ph-bell"></i>
|
||||
</button>
|
||||
<div
|
||||
x-show="openMessages"
|
||||
@click.away="openMessages = false"
|
||||
x-cloak
|
||||
class="popup absolute top-[calc(100%+0.5rem)] right-0 w-56"
|
||||
x-transition:enter="transition ease-out duration-100"
|
||||
x-transition:enter-start="opacity-0 scale-95"
|
||||
x-transition:enter-end="opacity-100 scale-100"
|
||||
x-transition:leave="transition ease-in duration-75"
|
||||
x-transition:leave-start="opacity-100 scale-100"
|
||||
x-transition:leave-end="opacity-0 scale-95"
|
||||
>
|
||||
<div class="flex items-center justify-center h-40 text-sm text-white/50">
|
||||
Keine Nachrichten
|
||||
</div>
|
||||
@else
|
||||
<div x-data="{ openMenu: false }" class="relative flex items-center">
|
||||
<button @click="openMenu = !openMenu"
|
||||
class="flex items-center focus:outline-none">
|
||||
<img class="size-8 rounded-full object-cover shadow-2xl"
|
||||
src="https://i.pravatar.cc/100"
|
||||
alt="Avatar"/>
|
||||
<svg
|
||||
class="size-4 ml-2 text-gray-500 transform transition-transform duration-200"
|
||||
:class="{'rotate-180': open}"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M19 9l-7 7-7-7"/>
|
||||
</svg>
|
||||
</button>
|
||||
<div
|
||||
x-show="openMenu"
|
||||
@click.away="openMenu = false"
|
||||
x-cloak
|
||||
class="popup absolute top-16 right-0 w-48"
|
||||
x-transition:enter="transition ease-out duration-100"
|
||||
x-transition:enter-start="opacity-0 transform scale-95"
|
||||
x-transition:enter-end="opacity-100 transform scale-100"
|
||||
x-transition:leave="transition ease-in duration-75"
|
||||
x-transition:leave-start="opacity-100 transform scale-100"
|
||||
x-transition:leave-end="opacity-0 transform scale-95"
|
||||
>
|
||||
@foreach($section['sub'] as $sub)
|
||||
<a href="{{ route($sub['route']) }}"
|
||||
class="block px-4 py-2 text-xs #text-gray-700 #hover:bg-gray-100 #hover:text-black #hover:bg-gradient-to-r #hover:from-[rgba(var(--accent))] #hover:to-transparent">
|
||||
<div class="flex items-center gap-3">
|
||||
<span>Logo</span>
|
||||
{{-- <span><x-dynamic-component :component="$sub['icon']"--}}
|
||||
{{-- class="!size-4"/></span>--}}
|
||||
<span>{{ $sub['title'] }}</span>
|
||||
</div>
|
||||
</a>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@else
|
||||
{{-- Avatar-Menü --}}
|
||||
<div x-data="{ openMenu: false }" class="relative flex items-center">
|
||||
<button
|
||||
@click="openMenu = !openMenu"
|
||||
class="flex items-center gap-1.5 focus:outline-none"
|
||||
>
|
||||
<img
|
||||
class="size-8 rounded-full object-cover shadow-2xl"
|
||||
src="https://i.pravatar.cc/100"
|
||||
alt="Avatar"
|
||||
/>
|
||||
<svg
|
||||
class="size-4 text-white/40 transition-transform duration-200"
|
||||
:class="{ 'rotate-180': openMenu }"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/>
|
||||
</svg>
|
||||
</button>
|
||||
<div
|
||||
x-show="openMenu"
|
||||
@click.away="openMenu = false"
|
||||
x-cloak
|
||||
class="popup absolute top-[calc(100%+0.5rem)] right-0 w-48"
|
||||
x-transition:enter="transition ease-out duration-100"
|
||||
x-transition:enter-start="opacity-0 scale-95"
|
||||
x-transition:enter-end="opacity-100 scale-100"
|
||||
x-transition:leave="transition ease-in duration-75"
|
||||
x-transition:leave-start="opacity-100 scale-100"
|
||||
x-transition:leave-end="opacity-0 scale-95"
|
||||
>
|
||||
@foreach($section['sub'] as $sub)
|
||||
<a href="{{ route($sub['route']) }}" class="block px-4 py-2 text-xs hover:bg-white/5">
|
||||
<div class="flex items-center gap-3">
|
||||
<i class="ph {{ $sub['icon'] ?? 'ph-circle' }} text-sm text-white/50"></i>
|
||||
<span>{{ $sub['title'] }}</span>
|
||||
</div>
|
||||
</a>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
@endforeach
|
||||
|
||||
</div>
|
||||
</nav>
|
||||
</header>
|
||||
|
|
|
|||
|
|
@ -160,23 +160,43 @@
|
|||
<div class="mw-main">
|
||||
|
||||
<header class="mw-topbar">
|
||||
<div class="mw-breadcrumb">
|
||||
<button class="mw-hamburger" style="display:none" onclick="document.getElementById('mw-sidebar').classList.add('open');document.getElementById('mw-overlay').classList.add('open');">
|
||||
<svg width="18" height="18" viewBox="0 0 18 18" fill="none"><path d="M2 4h14M2 9h14M2 14h14" stroke="currentColor" stroke-width="1.4" stroke-linecap="round"/></svg>
|
||||
|
||||
{{-- Links: Hamburger + Titel --}}
|
||||
<div class="mw-tb-left">
|
||||
<button class="mw-hamburger" onclick="document.getElementById('mw-sidebar').classList.add('open');document.getElementById('mw-overlay').classList.add('open');" aria-label="Menü">
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M2 4h12M2 8h12M2 12h12" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></svg>
|
||||
</button>
|
||||
@hasSection('breadcrumb-parent')@yield('breadcrumb-parent')@else{{ $breadcrumbParent ?? 'Dashboard' }}@endif
|
||||
<svg width="11" height="11" viewBox="0 0 11 11" fill="none"><path d="M3.5 2l4 3.5-4 3.5" stroke="var(--mw-b3)" stroke-width="1.3" stroke-linecap="round"/></svg>
|
||||
<b>@hasSection('breadcrumb')@yield('breadcrumb')@else{{ $breadcrumb ?? 'Übersicht' }}@endif</b>
|
||||
<div class="mw-tb-title">
|
||||
<span class="mw-tb-parent">@hasSection('breadcrumb-parent')@yield('breadcrumb-parent')@else{{ $breadcrumbParent ?? 'Dashboard' }}@endif</span>
|
||||
<svg class="mw-tb-sep" width="10" height="10" viewBox="0 0 10 10" fill="none"><path d="M3 2l3.5 3L3 8" stroke="currentColor" stroke-width="1.4" stroke-linecap="round"/></svg>
|
||||
<span class="mw-tb-current">@hasSection('breadcrumb')@yield('breadcrumb')@else{{ $breadcrumb ?? 'Übersicht' }}@endif</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mw-topbar-right">
|
||||
<div class="mw-live"><div class="mw-live-dot"></div>Live</div>
|
||||
<button wire:click="$dispatch('openModal',{component:'ui.search.modal.search-palette-modal'})" class="mw-search">
|
||||
<svg width="12" height="12" viewBox="0 0 12 12" fill="none"><circle cx="5" cy="5" r="3.8" stroke="currentColor" stroke-width="1.2"/><path d="M8 8l2.5 2.5" stroke="currentColor" stroke-width="1.2" stroke-linecap="round"/></svg>
|
||||
Suche <kbd>⌘K</kbd>
|
||||
|
||||
{{-- Rechts: Actions --}}
|
||||
<div class="mw-tb-right">
|
||||
|
||||
{{-- Live-Status --}}
|
||||
<div class="mw-tb-live">
|
||||
<span class="mw-tb-live-dot"></span>
|
||||
<span class="mw-tb-live-label">Live</span>
|
||||
</div>
|
||||
|
||||
{{-- Suche --}}
|
||||
<button class="mw-tb-search" onclick="Livewire.dispatch('openModal',{component:'ui.search.modal.search-palette-modal'})">
|
||||
<svg width="13" height="13" viewBox="0 0 12 12" fill="none"><circle cx="5" cy="5" r="3.5" stroke="currentColor" stroke-width="1.3"/><path d="M7.8 7.8l2.2 2.2" stroke="currentColor" stroke-width="1.3" stroke-linecap="round"/></svg>
|
||||
<span class="mw-tb-search-text">Suche</span>
|
||||
<kbd class="mw-tb-kbd">⌘K</kbd>
|
||||
</button>
|
||||
<div class="mw-ip">{{ gethostname() ?: '—' }}</div>
|
||||
<button class="mw-btn-primary" onclick="Livewire.dispatch('openModal',{component:'ui.domain.modal.domain-create-modal'})">+ Domain</button>
|
||||
|
||||
{{-- Domain erstellen --}}
|
||||
<button class="mw-tb-add" onclick="Livewire.dispatch('openModal',{component:'ui.domain.modal.domain-create-modal'})">
|
||||
<svg width="11" height="11" viewBox="0 0 11 11" fill="none"><path d="M5.5 1v9M1 5.5h9" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"/></svg>
|
||||
<span class="mw-tb-add-text">Domain</span>
|
||||
</button>
|
||||
|
||||
</div>
|
||||
|
||||
</header>
|
||||
|
||||
<div class="mw-content">
|
||||
|
|
|
|||
|
|
@ -213,8 +213,25 @@
|
|||
</div>
|
||||
<span class="mw-sc-badge mw-badge-mute">{{ $alertCount }} offen</span>
|
||||
</div>
|
||||
<div class="mw-sc-body" style="font-style:italic;color:var(--mw-t4);">
|
||||
{{ $alertCount === 0 ? 'Keine Warnungen.' : ($alertCount . ' aktive Warnungen.') }}
|
||||
<div class="mw-sc-body">
|
||||
@if($sandboxAlerts->isEmpty())
|
||||
<div style="font-style:italic;color:var(--mw-t4);font-size:12px;">Keine Warnungen.</div>
|
||||
@else
|
||||
<div style="display:flex;flex-direction:column;gap:5px;">
|
||||
@foreach($sandboxAlerts as $sr)
|
||||
<div style="display:flex;align-items:center;gap:8px;padding:5px 8px;background:rgba(234,179,8,.07);border:1px solid rgba(234,179,8,.2);border-radius:6px;">
|
||||
<svg width="11" height="11" viewBox="0 0 11 11" fill="none" style="flex-shrink:0;color:#fbbf24"><path d="M5.5 1L10.5 10H.5L5.5 1Z" stroke="currentColor" stroke-width="1.2" stroke-linejoin="round"/><path d="M5.5 4.5v2.5" stroke="currentColor" stroke-width="1.2" stroke-linecap="round"/></svg>
|
||||
<span style="font-size:11.5px;color:#fbbf24;font-weight:500;">
|
||||
@if($sr->type === 'global') Sandbox global aktiv
|
||||
@elseif($sr->type === 'domain') Sandbox aktiv: {{ $sr->target }}
|
||||
@else Sandbox aktiv: {{ $sr->target }}
|
||||
@endif
|
||||
</span>
|
||||
<a href="{{ route('ui.system.sandbox') }}" style="margin-left:auto;font-size:10.5px;color:rgba(251,191,36,.6);text-decoration:none;white-space:nowrap;">verwalten →</a>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -138,6 +138,15 @@ $_wmUrl = ($_wmSub && $_wmBase)
|
|||
|
||||
<td class="mbx-td">
|
||||
<div style="display:flex;align-items:center;gap:8px;min-width:0">
|
||||
@if(\App\Models\SandboxRoute::isActiveForDomain($d->domain))
|
||||
<div class="dom-icon" style="color:#fbbf24;background:rgba(234,179,8,.1);border-color:rgba(234,179,8,.3);flex-shrink:0">
|
||||
<svg width="12" height="12" viewBox="0 0 14 14" fill="none">
|
||||
<path d="M7 2L13 12H1L7 2Z" stroke="currentColor" stroke-width="1.2" stroke-linejoin="round"/>
|
||||
<path d="M7 6v3" stroke="currentColor" stroke-width="1.2" stroke-linecap="round"/>
|
||||
<circle cx="7" cy="10.5" r=".6" fill="currentColor"/>
|
||||
</svg>
|
||||
</div>
|
||||
@else
|
||||
<div class="dom-icon">
|
||||
<svg width="12" height="12" viewBox="0 0 14 14" fill="none">
|
||||
<circle cx="7" cy="7" r="5.5" stroke="currentColor" stroke-width="1.2"/>
|
||||
|
|
@ -145,6 +154,7 @@ $_wmUrl = ($_wmSub && $_wmBase)
|
|||
<path d="M1.5 5.5h11M1.5 8.5h11" stroke="currentColor" stroke-width="1.2"/>
|
||||
</svg>
|
||||
</div>
|
||||
@endif
|
||||
<div style="min-width:0">
|
||||
<div class="dom-name">{{ $d->domain }}</div>
|
||||
@if(!empty($d->description))
|
||||
|
|
|
|||
|
|
@ -2,7 +2,8 @@
|
|||
<x-slot:breadcrumb>API Keys</x-slot:breadcrumb>
|
||||
|
||||
<div x-data="{activeGroup: 'mailboxes'}"
|
||||
x-on:token-created.window="$dispatch('openModal', {component: 'ui.system.modal.api-key-show-modal', arguments: {plainText: $event.detail.plainText}})">
|
||||
x-on:token-created.window="$wire.$refresh()"
|
||||
x-on:token-deleted.window="$wire.$refresh()">
|
||||
|
||||
<div class="mbx-page-header">
|
||||
<div class="mbx-page-title">
|
||||
|
|
@ -23,7 +24,7 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div style="display:grid;grid-template-columns:1fr 340px;gap:14px;align-items:start">
|
||||
<div class="mw-apikey-layout">
|
||||
|
||||
{{-- ═══ Left: Keys + Endpoint-Docs ═══ --}}
|
||||
<div class="mbx-sections">
|
||||
|
|
@ -46,62 +47,68 @@
|
|||
<div style="font-size:11.5px;color:var(--mw-t4)">Erstelle deinen ersten Key um externe Anwendungen zu verbinden.</div>
|
||||
</div>
|
||||
@else
|
||||
<div class="mbx-table-wrap">
|
||||
<table class="mbx-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="mbx-th">Name</th>
|
||||
<th class="mbx-th">Scopes</th>
|
||||
<th class="mbx-th" style="width:86px">Modus</th>
|
||||
<th class="mbx-th" style="width:120px">Zuletzt genutzt</th>
|
||||
<th class="mbx-th" style="width:110px">Erstellt</th>
|
||||
<th class="mbx-th mbx-th-right" style="width:50px"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach($tokens as $token)
|
||||
<tr class="mbx-tr">
|
||||
<td class="mbx-td">
|
||||
<div style="display:flex;align-items:center;gap:8px">
|
||||
<div style="width:26px;height:26px;border-radius:7px;background:var(--mw-bg4);border:1px solid var(--mw-b2);display:flex;align-items:center;justify-content:center;flex-shrink:0">
|
||||
<svg width="12" height="12" viewBox="0 0 16 16" fill="none">
|
||||
<path d="M6 10a4 4 0 1 0 0-8 4 4 0 0 0 0 8Z" stroke="var(--mw-t3)" stroke-width="1.3"/>
|
||||
<path d="M9.5 8.5L14 13" stroke="var(--mw-t3)" stroke-width="1.3" stroke-linecap="round"/>
|
||||
</svg>
|
||||
</div>
|
||||
<span style="font-size:12.5px;font-weight:500;color:var(--mw-t1)">{{ $token->name }}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td class="mbx-td">
|
||||
<div style="display:flex;flex-wrap:wrap;gap:3px">
|
||||
@foreach($token->abilities as $scope)
|
||||
<span style="font-family:monospace;font-size:9.5px;padding:1px 5px;background:var(--mw-bg4);border:1px solid var(--mw-b2);border-radius:4px;color:var(--mw-t4)">{{ $scope }}</span>
|
||||
@endforeach
|
||||
</div>
|
||||
</td>
|
||||
<td class="mbx-td">
|
||||
@if($token->sandbox)
|
||||
<span style="font-size:10px;padding:2px 7px;border-radius:5px;background:rgba(251,191,36,.1);border:1px solid rgba(251,191,36,.25);color:#f59e0b;font-weight:500">Sandbox</span>
|
||||
@else
|
||||
<span style="font-size:10px;padding:2px 7px;border-radius:5px;background:rgba(16,185,129,.08);border:1px solid rgba(16,185,129,.2);color:#34d399;font-weight:500">Live</span>
|
||||
@endif
|
||||
</td>
|
||||
<td class="mbx-td mbx-td-muted" style="font-size:11px">{{ $token->last_used_at?->diffForHumans() ?? '—' }}</td>
|
||||
<td class="mbx-td mbx-td-muted" style="font-size:11px">{{ $token->created_at->format('d.m.Y') }}</td>
|
||||
<td class="mbx-td">
|
||||
<div class="mbx-actions">
|
||||
<button wire:click="deleteToken({{ $token->id }})"
|
||||
wire:confirm="API Key '{{ $token->name }}' wirklich löschen?"
|
||||
class="mbx-act-btn mbx-act-danger" title="Löschen">
|
||||
<svg width="12" height="12" viewBox="0 0 13 13" fill="none"><path d="M2 3h9M5 3V2h3v1M3.5 3l.5 8h5l.5-8" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
{{-- Header --}}
|
||||
<div class="mw-kl-head">
|
||||
<span>Name</span>
|
||||
<span>Scopes</span>
|
||||
<span>Modus</span>
|
||||
<span>Erstellt</span>
|
||||
<span></span>
|
||||
</div>
|
||||
|
||||
{{-- Rows --}}
|
||||
@foreach($tokens as $token)
|
||||
@php
|
||||
$abilities = $token->abilities;
|
||||
$visible = array_slice($abilities, 0, 2);
|
||||
$rest = count($abilities) - 2;
|
||||
@endphp
|
||||
<div class="mw-kl-row">
|
||||
{{-- Name --}}
|
||||
<div class="mw-kl-name" style="display:flex;align-items:center;gap:8px;overflow:hidden">
|
||||
<div style="width:26px;height:26px;border-radius:7px;background:var(--mw-bg4);border:1px solid var(--mw-b2);display:flex;align-items:center;justify-content:center;flex-shrink:0">
|
||||
<svg width="12" height="12" viewBox="0 0 16 16" fill="none">
|
||||
<path d="M6 10a4 4 0 1 0 0-8 4 4 0 0 0 0 8Z" stroke="var(--mw-t3)" stroke-width="1.3"/>
|
||||
<path d="M9.5 8.5L14 13" stroke="var(--mw-t3)" stroke-width="1.3" stroke-linecap="round"/>
|
||||
</svg>
|
||||
</div>
|
||||
<span style="font-size:12.5px;font-weight:500;color:var(--mw-t1);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;min-width:0;flex:1">{{ $token->name }}</span>
|
||||
</div>
|
||||
{{-- Scopes --}}
|
||||
<div class="mw-kl-scopes" style="display:flex;flex-wrap:nowrap;gap:3px;align-items:center;overflow:hidden">
|
||||
@foreach($visible as $scope)
|
||||
@php $short = collect(explode(':', $scope))->map(fn($p) => $p[0])->join(':'); @endphp
|
||||
<span title="{{ $scope }}" style="font-family:monospace;font-size:9.5px;padding:1px 5px;background:var(--mw-bg4);border:1px solid var(--mw-b2);border-radius:4px;color:var(--mw-t4)">{{ $short }}</span>
|
||||
@endforeach
|
||||
@if($rest > 0)
|
||||
<button wire:click="$dispatch('openModal',{component:'ui.system.modal.api-key-scopes-modal',arguments:{tokenId:{{ $token->id }}}})"
|
||||
style="font-family:monospace;font-size:9.5px;padding:1px 6px;background:var(--mw-bg4);border:1px solid var(--mw-b2);border-radius:4px;color:var(--mw-v2);cursor:pointer;white-space:nowrap;flex-shrink:0">
|
||||
+{{ $rest }}
|
||||
</button>
|
||||
@endif
|
||||
<span class="mw-kl-date-inline" style="font-size:10px;color:var(--mw-t5);margin-left:2px">· {{ $token->created_at->format('d.m.Y') }}</span>
|
||||
</div>
|
||||
{{-- Modus --}}
|
||||
<div class="mw-kl-modus">
|
||||
@if($token->sandbox)
|
||||
<span style="font-size:10px;padding:2px 7px;border-radius:5px;background:rgba(251,191,36,.1);border:1px solid rgba(251,191,36,.25);color:#f59e0b;font-weight:500;white-space:nowrap">Sandbox</span>
|
||||
@else
|
||||
<span style="font-size:10px;padding:2px 7px;border-radius:5px;background:rgba(16,185,129,.08);border:1px solid rgba(16,185,129,.2);color:#34d399;font-weight:500;white-space:nowrap">Live</span>
|
||||
@endif
|
||||
</div>
|
||||
{{-- Date (desktop only) --}}
|
||||
<div class="mw-kl-date" style="font-size:11px;color:var(--mw-t4)">{{ $token->created_at->format('d.m.Y') }}</div>
|
||||
{{-- Actions --}}
|
||||
<div class="mw-kl-actions">
|
||||
<button wire:click="$dispatch('openModal',{component:'ui.system.modal.api-key-delete-modal',arguments:{tokenId:{{ $token->id }}}})"
|
||||
class="mbx-act-btn mbx-act-danger" title="Löschen">
|
||||
<svg width="12" height="12" viewBox="0 0 13 13" fill="none"><path d="M2 3h9M5 3V2h3v1M3.5 3l.5 8h5l.5-8" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
|
||||
@endif
|
||||
</div>
|
||||
|
||||
|
|
@ -293,4 +300,5 @@ Content-Type: application/json</pre>
|
|||
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -18,33 +18,37 @@
|
|||
@error('name') <div class="mw-modal-error">{{ $message }}</div> @enderror
|
||||
</div>
|
||||
|
||||
<div style="margin-top:16px">
|
||||
<div style="margin-top:14px">
|
||||
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:8px">
|
||||
<label class="mw-modal-label" style="margin:0">Berechtigungen (Scopes)</label>
|
||||
<label class="mw-modal-label" style="margin:0">Berechtigungen</label>
|
||||
<button type="button" wire:click="toggleAll"
|
||||
style="background:none;border:none;font-size:11px;color:var(--mw-v);cursor:pointer;padding:0">
|
||||
{{ count($selected) === count($scopes) ? 'Alle abwählen' : 'Alle wählen' }}
|
||||
</button>
|
||||
</div>
|
||||
<div style="display:flex;flex-direction:column;gap:6px;padding:12px;background:var(--mw-bg4);border:1px solid var(--mw-b2);border-radius:8px">
|
||||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:6px;padding:12px;background:var(--mw-bg4);border:1px solid var(--mw-b2);border-radius:8px">
|
||||
@foreach($scopes as $key => $label)
|
||||
<label style="display:flex;align-items:center;gap:8px;cursor:pointer">
|
||||
<label style="display:flex;align-items:center;gap:7px;cursor:pointer;padding:5px 8px;border-radius:5px;transition:background .1s"
|
||||
onmouseover="this.style.background='var(--mw-bg3)'"
|
||||
onmouseout="this.style.background='transparent'">
|
||||
<input type="checkbox" wire:model="selected" value="{{ $key }}"
|
||||
style="width:13px;height:13px;accent-color:var(--mw-v)">
|
||||
<span style="font-size:12px;color:var(--mw-t2);font-family:monospace">{{ $key }}</span>
|
||||
<span style="font-size:11px;color:var(--mw-t4)">— {{ $label }}</span>
|
||||
style="width:13px;height:13px;flex-shrink:0;accent-color:var(--mw-v)">
|
||||
<div>
|
||||
<div style="font-size:11px;color:var(--mw-t2);font-family:monospace;line-height:1.3">{{ $key }}</div>
|
||||
<div style="font-size:10px;color:var(--mw-t4);line-height:1.3">{{ $label }}</div>
|
||||
</div>
|
||||
</label>
|
||||
@endforeach
|
||||
</div>
|
||||
@error('selected') <div class="mw-modal-error">{{ $message }}</div> @enderror
|
||||
</div>
|
||||
|
||||
<div style="margin-top:14px;padding:12px;background:rgba(251,191,36,.06);border:1px solid rgba(251,191,36,.2);border-radius:8px">
|
||||
<label style="display:flex;align-items:flex-start;gap:8px;cursor:pointer">
|
||||
<input type="checkbox" wire:model="sandbox" style="width:13px;height:13px;margin-top:1px;accent-color:#f59e0b">
|
||||
<div style="margin-top:12px">
|
||||
<label style="display:flex;align-items:center;gap:10px;cursor:pointer;padding:10px 12px;background:rgba(251,191,36,.05);border:1px solid rgba(251,191,36,.18);border-radius:8px">
|
||||
<input type="checkbox" wire:model="sandbox" style="width:13px;height:13px;flex-shrink:0;accent-color:#f59e0b">
|
||||
<div>
|
||||
<div style="font-size:12.5px;color:var(--mw-t1);font-weight:500">Sandbox-Modus</div>
|
||||
<div style="font-size:11px;color:var(--mw-t4);margin-top:2px">Schreiboperationen werden simuliert — keine Änderungen in der Datenbank. Ideal für Tests und Entwicklung.</div>
|
||||
<div style="font-size:12px;color:var(--mw-t1);font-weight:500">Sandbox-Modus</div>
|
||||
<div style="font-size:10.5px;color:var(--mw-t4);margin-top:1px">Schreiboperationen werden simuliert — keine Änderungen in der DB.</div>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,30 @@
|
|||
<div class="mw-modal-frame">
|
||||
|
||||
<div class="mw-modal-head">
|
||||
<div style="display:flex;align-items:center;gap:7px">
|
||||
<span class="mbx-badge-mute">API Key</span>
|
||||
<span style="font-size:12px;color:var(--mw-t3)">— Löschen</span>
|
||||
</div>
|
||||
<button wire:click="$dispatch('closeModal')" class="mw-modal-close">
|
||||
<svg width="13" height="13" viewBox="0 0 13 13" fill="none"><path d="M1 1l11 11M12 1L1 12" stroke="currentColor" stroke-width="1.4" stroke-linecap="round"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="mw-modal-body">
|
||||
<div style="padding:12px 14px;border-radius:8px;border:1px solid rgba(239,68,68,.25);background:rgba(239,68,68,.06)">
|
||||
<div style="font-size:12.5px;font-weight:500;color:var(--mw-t1);font-family:monospace">{{ $tokenName }}</div>
|
||||
<div style="font-size:11px;color:rgba(252,165,165,.8);margin-top:4px">
|
||||
Dieser API Key wird dauerhaft gelöscht. Anwendungen die ihn nutzen verlieren sofort den Zugriff.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mw-modal-foot">
|
||||
<button wire:click="$dispatch('closeModal')" class="mw-btn-cancel">Abbrechen</button>
|
||||
<button wire:click="delete" class="mw-btn-del">
|
||||
<svg width="12" height="12" viewBox="0 0 13 13" fill="none"><path d="M2 3h9M5 3V2h3v1M3.5 3l.5 8h5l.5-8" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/></svg>
|
||||
Löschen
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
<div class="mw-modal-frame">
|
||||
|
||||
<div class="mw-modal-head">
|
||||
<div style="display:flex;align-items:center;gap:7px">
|
||||
<span class="mbx-badge-mute">{{ $tokenName }}</span>
|
||||
<span style="font-size:12px;color:var(--mw-t3)">— Scopes</span>
|
||||
</div>
|
||||
<button wire:click="$dispatch('closeModal')" class="mw-modal-close">
|
||||
<svg width="13" height="13" viewBox="0 0 13 13" fill="none"><path d="M1 1l11 11M12 1L1 12" stroke="currentColor" stroke-width="1.4" stroke-linecap="round"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="mw-modal-body">
|
||||
<div style="display:flex;flex-direction:column;gap:5px">
|
||||
@foreach($scopes as $scope)
|
||||
@php $short = collect(explode(':', $scope))->map(fn($p) => $p[0])->join(':'); @endphp
|
||||
<div style="display:flex;align-items:center;gap:10px;padding:8px 10px;background:var(--mw-bg4);border:1px solid var(--mw-b2);border-radius:7px">
|
||||
<code style="font-family:monospace;font-size:11px;padding:1px 7px;background:var(--mw-bg3);border:1px solid var(--mw-b2);border-radius:4px;color:var(--mw-t4);flex-shrink:0">{{ $short }}</code>
|
||||
<span style="font-size:11.5px;color:var(--mw-t3)">{{ $scope }}</span>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mw-modal-foot">
|
||||
<button wire:click="$dispatch('closeModal')" class="mw-btn-cancel">Schließen</button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
|
@ -5,7 +5,7 @@
|
|||
<span class="mbx-badge-ok">Key erstellt</span>
|
||||
<span style="font-size:12px;color:var(--mw-t3)">— einmalig anzeigen</span>
|
||||
</div>
|
||||
<button wire:click="$dispatch('closeModal')" class="mw-modal-close">
|
||||
<button wire:click="dismiss" class="mw-modal-close">
|
||||
<svg width="13" height="13" viewBox="0 0 13 13" fill="none"><path d="M1 1l11 11M12 1L1 12" stroke="currentColor" stroke-width="1.4" stroke-linecap="round"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
|
|
@ -37,7 +37,7 @@
|
|||
</div>
|
||||
|
||||
<div class="mw-modal-foot">
|
||||
<button wire:click="$dispatch('closeModal')" class="mbx-btn-primary">Fertig</button>
|
||||
<button wire:click="dismiss" class="mw-btn-primary">Fertig</button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -30,11 +30,16 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Sandbox-Routing Regeln --}}
|
||||
<livewire:ui.system.sandbox-routes />
|
||||
|
||||
<div style="height:1px;background:var(--mw-b1);margin-bottom:14px"></div>
|
||||
|
||||
{{-- Mail-Client Layout --}}
|
||||
<div style="display:grid;grid-template-columns:320px 1fr;gap:0;border:1px solid var(--mw-b1);border-radius:10px;overflow:hidden;background:var(--mw-bg2);min-height:520px">
|
||||
<div class="mw-sandbox-client">
|
||||
|
||||
{{-- ═══ Left: Mail-Liste ═══ --}}
|
||||
<div style="border-right:1px solid var(--mw-b1);overflow-y:auto;max-height:680px">
|
||||
<div class="mw-sandbox-list">
|
||||
|
||||
@if($mails->isEmpty())
|
||||
<div style="padding:48px 16px;text-align:center">
|
||||
|
|
@ -83,7 +88,7 @@
|
|||
</div>
|
||||
|
||||
{{-- ═══ Right: Mail-Detail ═══ --}}
|
||||
<div style="overflow-y:auto;max-height:680px">
|
||||
<div class="mw-sandbox-detail">
|
||||
|
||||
@if(!$selected)
|
||||
<div style="display:flex;align-items:center;justify-content:center;height:100%;min-height:400px;flex-direction:column;gap:12px">
|
||||
|
|
@ -189,7 +194,7 @@
|
|||
<span style="font-size:11px;color:var(--mw-t4);margin-left:6px">— Sandbox-Transport aktivieren</span>
|
||||
</div>
|
||||
</div>
|
||||
<div style="padding:16px 18px;display:grid;grid-template-columns:1fr 1fr 1fr;gap:16px">
|
||||
<div class="mw-postfix-grid">
|
||||
|
||||
<div>
|
||||
<div style="font-size:11px;color:var(--mw-t4);margin-bottom:6px;font-weight:500">1. master.cf — Transport anlegen</div>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,113 @@
|
|||
<div style="margin-bottom:14px">
|
||||
|
||||
{{-- ── Header ── --}}
|
||||
<div class="mbx-domain-head" style="margin-bottom:10px">
|
||||
<div class="mbx-domain-info">
|
||||
<span style="font-size:10.5px;padding:2px 8px;border-radius:4px;background:rgba(234,179,8,.12);border:1px solid rgba(234,179,8,.3);color:#fbbf24;font-weight:600;">
|
||||
Sandbox-Routing
|
||||
</span>
|
||||
<span style="font-size:11px;color:var(--mw-t4);margin-left:6px">— Transport-Regeln für Postfix</span>
|
||||
</div>
|
||||
<button wire:click="syncPostfix" class="mw-btn-secondary"
|
||||
style="display:flex;align-items:center;gap:6px;font-size:12px;padding:5px 12px;white-space:nowrap">
|
||||
<svg width="12" height="12" viewBox="0 0 14 14" fill="none">
|
||||
<path d="M12.5 7A5.5 5.5 0 1 1 7 1.5" stroke="currentColor" stroke-width="1.2" stroke-linecap="round"/>
|
||||
<path d="M7 .5l2.5 1.5L7 3.5" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
Postfix-Sync
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{{-- ── Sync-Message ── --}}
|
||||
@if($syncMessage)
|
||||
<div style="margin-bottom:10px;padding:7px 12px;border-radius:6px;font-size:11.5px;display:flex;align-items:center;gap:7px;
|
||||
{{ $syncOk
|
||||
? 'background:rgba(34,197,94,.07);border:1px solid rgba(34,197,94,.25);color:var(--mw-gr)'
|
||||
: 'background:rgba(239,68,68,.07);border:1px solid rgba(239,68,68,.25);color:#f87171' }}">
|
||||
@if($syncOk)
|
||||
<svg width="11" height="11" viewBox="0 0 11 11" fill="none"><path d="M1.5 5.5l3 3 5-5" stroke="currentColor" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round"/></svg>
|
||||
@else
|
||||
<svg width="11" height="11" viewBox="0 0 11 11" fill="none"><path d="M1.5 1.5l8 8M9.5 1.5l-8 8" stroke="currentColor" stroke-width="1.3" stroke-linecap="round"/></svg>
|
||||
@endif
|
||||
{{ $syncMessage }}
|
||||
</div>
|
||||
@endif
|
||||
|
||||
{{-- ── Add-Form ── --}}
|
||||
<div style="display:flex;gap:8px;align-items:center;margin-bottom:12px;flex-wrap:wrap">
|
||||
<select wire:model.live="newType"
|
||||
style="font-size:12px;padding:5px 8px;border-radius:6px;background:var(--mw-bg3);border:1px solid var(--mw-b2);color:var(--mw-t2);cursor:pointer;outline:none;height:30px;flex-shrink:0">
|
||||
<option value="global">global</option>
|
||||
<option value="domain">domain</option>
|
||||
<option value="address">address</option>
|
||||
</select>
|
||||
@if($newType !== 'global')
|
||||
<input wire:model="newTarget" type="text"
|
||||
placeholder="{{ $newType === 'domain' ? 'example.com' : 'user@example.com' }}"
|
||||
style="font-size:12px;padding:5px 10px;border-radius:6px;background:var(--mw-bg3);border:1px solid var(--mw-b2);color:var(--mw-t1);outline:none;width:220px;height:30px;flex-shrink:0"
|
||||
wire:keydown.enter="addRoute">
|
||||
@endif
|
||||
<button wire:click="addRoute" class="mw-btn-primary" style="font-size:12px;padding:5px 14px;height:30px;white-space:nowrap;flex-shrink:0">
|
||||
+ Hinzufügen
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{{-- ── Route List ── --}}
|
||||
@if($this->routes->isEmpty())
|
||||
<div style="padding:20px 16px;text-align:center;color:var(--mw-t4);font-size:12px;background:var(--mw-bg3);border:1px solid var(--mw-b1);border-radius:8px;">
|
||||
Keine Sandbox-Routen konfiguriert.
|
||||
</div>
|
||||
@else
|
||||
<div style="border:1px solid var(--mw-b1);border-radius:8px;overflow:hidden">
|
||||
@foreach($this->routes as $route)
|
||||
<div style="display:flex;align-items:center;gap:8px;padding:8px 12px;
|
||||
{{ !$loop->last ? 'border-bottom:1px solid var(--mw-b1);' : '' }}
|
||||
background:{{ $route->is_active ? 'var(--mw-bg3)' : 'var(--mw-bg4)' }}">
|
||||
|
||||
{{-- Type badge --}}
|
||||
@if($route->type === 'global')
|
||||
<span style="font-size:9.5px;padding:1px 6px;border-radius:4px;background:rgba(239,68,68,.12);border:1px solid rgba(239,68,68,.25);color:#f87171;font-weight:600;flex-shrink:0">global</span>
|
||||
@elseif($route->type === 'domain')
|
||||
<span style="font-size:9.5px;padding:1px 6px;border-radius:4px;background:rgba(124,58,237,.12);border:1px solid rgba(124,58,237,.25);color:var(--mw-v2);font-weight:600;flex-shrink:0">domain</span>
|
||||
@else
|
||||
<span style="font-size:9.5px;padding:1px 6px;border-radius:4px;background:rgba(59,130,246,.12);border:1px solid rgba(59,130,246,.25);color:#60a5fa;font-weight:600;flex-shrink:0">address</span>
|
||||
@endif
|
||||
|
||||
{{-- Target --}}
|
||||
<span style="font-size:12px;color:{{ $route->is_active ? 'var(--mw-t1)' : 'var(--mw-t4)' }};font-family:monospace;flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">
|
||||
{{ $route->target ?? '* (alle)' }}
|
||||
</span>
|
||||
|
||||
{{-- Active indicator --}}
|
||||
@if($route->is_active)
|
||||
<span style="font-size:9.5px;padding:1px 6px;border-radius:4px;background:rgba(34,197,94,.1);border:1px solid rgba(34,197,94,.2);color:var(--mw-gr);font-weight:600;flex-shrink:0">aktiv</span>
|
||||
@else
|
||||
<span style="font-size:9.5px;padding:1px 6px;border-radius:4px;background:var(--mw-bg4);border:1px solid var(--mw-b2);color:var(--mw-t4);font-weight:600;flex-shrink:0">inaktiv</span>
|
||||
@endif
|
||||
|
||||
{{-- Toggle button --}}
|
||||
<button wire:click="toggleRoute({{ $route->id }})" class="mbx-act-btn"
|
||||
title="{{ $route->is_active ? 'Deaktivieren' : 'Aktivieren' }}"
|
||||
style="flex-shrink:0">
|
||||
@if($route->is_active)
|
||||
<svg width="12" height="12" viewBox="0 0 14 14" fill="none"><rect x="1" y="4" width="12" height="6" rx="3" stroke="currentColor" stroke-width="1.2"/><circle cx="10" cy="7" r="2" fill="currentColor"/></svg>
|
||||
@else
|
||||
<svg width="12" height="12" viewBox="0 0 14 14" fill="none"><rect x="1" y="4" width="12" height="6" rx="3" stroke="currentColor" stroke-width="1.2"/><circle cx="4" cy="7" r="2" fill="currentColor" opacity=".4"/></svg>
|
||||
@endif
|
||||
</button>
|
||||
|
||||
{{-- Delete button --}}
|
||||
<button wire:click="removeRoute({{ $route->id }})"
|
||||
wire:confirm="Route '{{ $route->target ?? 'global' }}' wirklich löschen?"
|
||||
class="mbx-act-btn mbx-act-danger" title="Löschen" style="flex-shrink:0">
|
||||
<svg width="12" height="12" viewBox="0 0 13 13" fill="none">
|
||||
<path d="M2 3h9M5 3V2h3v1M3.5 3l.5 8h5l.5-8" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
@endif
|
||||
|
||||
</div>
|
||||
|
|
@ -23,7 +23,7 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div style="display:grid;grid-template-columns:1fr 300px;gap:14px;align-items:start">
|
||||
<div class="mw-wh-layout">
|
||||
|
||||
{{-- ═══ Left ═══ --}}
|
||||
<div class="mbx-sections">
|
||||
|
|
@ -45,83 +45,102 @@
|
|||
<div style="font-size:11.5px;color:var(--mw-t4)">Verbinde externe Systeme — sie werden bei Events automatisch benachrichtigt.</div>
|
||||
</div>
|
||||
@else
|
||||
<div class="mbx-table-wrap">
|
||||
<table class="mbx-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="mbx-th">Webhook</th>
|
||||
<th class="mbx-th">Events</th>
|
||||
<th class="mbx-th" style="width:80px">Status</th>
|
||||
<th class="mbx-th" style="width:90px">HTTP</th>
|
||||
<th class="mbx-th" style="width:130px">Zuletzt ausgelöst</th>
|
||||
<th class="mbx-th mbx-th-right" style="width:80px">Aktionen</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach($webhooks as $wh)
|
||||
<tr class="mbx-tr">
|
||||
<td class="mbx-td">
|
||||
<div style="display:flex;align-items:center;gap:8px">
|
||||
<div style="width:8px;height:8px;border-radius:50%;flex-shrink:0;background:{{ $wh->is_active ? '#34d399' : 'var(--mw-t4)' }};box-shadow:{{ $wh->is_active ? '0 0 6px rgba(52,211,153,.5)' : 'none' }}"></div>
|
||||
<div>
|
||||
<div style="font-size:12.5px;font-weight:500;color:var(--mw-t1)">{{ $wh->name }}</div>
|
||||
<div style="font-family:monospace;font-size:10.5px;color:var(--mw-t4);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:260px">{{ $wh->url }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="mbx-td">
|
||||
<div style="display:flex;flex-wrap:wrap;gap:3px">
|
||||
@foreach($wh->events as $ev)
|
||||
<span style="font-size:9.5px;padding:1px 5px;background:var(--mw-bg4);border:1px solid var(--mw-b2);border-radius:4px;color:var(--mw-t4);font-family:monospace">{{ $ev }}</span>
|
||||
@endforeach
|
||||
</div>
|
||||
</td>
|
||||
<td class="mbx-td">
|
||||
@if($wh->is_active)
|
||||
<span class="mbx-badge-ok">Aktiv</span>
|
||||
@else
|
||||
<span class="mbx-badge-warn">Pausiert</span>
|
||||
@endif
|
||||
</td>
|
||||
<td class="mbx-td">
|
||||
@if($wh->last_status === null)
|
||||
<span style="font-size:11px;color:var(--mw-t4)">—</span>
|
||||
@elseif($wh->last_status >= 200 && $wh->last_status < 300)
|
||||
<span style="font-family:monospace;font-size:11px;color:#34d399;font-weight:600">{{ $wh->last_status }}</span>
|
||||
@elseif($wh->last_status === 0)
|
||||
<span style="font-family:monospace;font-size:11px;color:#f87171;font-weight:600">Timeout</span>
|
||||
@else
|
||||
<span style="font-family:monospace;font-size:11px;color:#f87171;font-weight:600">{{ $wh->last_status }}</span>
|
||||
@endif
|
||||
</td>
|
||||
<td class="mbx-td mbx-td-muted" style="font-size:11px">
|
||||
{{ $wh->last_triggered_at?->diffForHumans() ?? '—' }}
|
||||
</td>
|
||||
<td class="mbx-td">
|
||||
<div class="mbx-actions">
|
||||
<button wire:click="$dispatch('openModal',{component:'ui.system.modal.webhook-edit-modal',arguments:{webhookId:{{ $wh->id }}}})"
|
||||
class="mbx-act-btn" title="Bearbeiten">
|
||||
<svg width="12" height="12" viewBox="0 0 13 13" fill="none"><path d="M9 2l2 2-7 7H2V9l7-7Z" stroke="currentColor" stroke-width="1.2" stroke-linejoin="round"/></svg>
|
||||
</button>
|
||||
<button wire:click="toggleActive({{ $wh->id }})"
|
||||
class="mbx-act-btn" title="{{ $wh->is_active ? 'Pausieren' : 'Aktivieren' }}">
|
||||
@if($wh->is_active)
|
||||
<svg width="12" height="12" viewBox="0 0 13 13" fill="none"><rect x="3" y="2" width="2.5" height="9" rx="1" stroke="currentColor" stroke-width="1.2"/><rect x="7.5" y="2" width="2.5" height="9" rx="1" stroke="currentColor" stroke-width="1.2"/></svg>
|
||||
@else
|
||||
<svg width="12" height="12" viewBox="0 0 13 13" fill="none"><path d="M4 2.5l7 4-7 4V2.5Z" stroke="currentColor" stroke-width="1.2" stroke-linejoin="round"/></svg>
|
||||
@endif
|
||||
</button>
|
||||
<button wire:click="$dispatch('openModal',{component:'ui.system.modal.webhook-delete-modal',arguments:{webhookId:{{ $wh->id }}}})"
|
||||
class="mbx-act-btn mbx-act-danger" title="Löschen">
|
||||
<svg width="12" height="12" viewBox="0 0 13 13" fill="none"><path d="M2 3h9M5 3V2h3v1M3.5 3l.5 8h5l.5-8" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
{{-- Header --}}
|
||||
<div class="mw-whl-head">
|
||||
<span>Webhook</span>
|
||||
<span>Events</span>
|
||||
<span>Status</span>
|
||||
<span>HTTP</span>
|
||||
<span>Aktionen</span>
|
||||
</div>
|
||||
|
||||
{{-- Rows --}}
|
||||
@foreach($webhooks as $wh)
|
||||
@php
|
||||
$evVisible = array_slice($wh->events, 0, 2);
|
||||
$evRest = count($wh->events) - 2;
|
||||
@endphp
|
||||
<div class="mw-whl-row">
|
||||
{{-- Webhook name + url --}}
|
||||
<div class="mw-whl-webhook" style="display:flex;align-items:center;gap:8px;min-width:0">
|
||||
<div style="width:8px;height:8px;border-radius:50%;flex-shrink:0;background:{{ $wh->is_active ? '#34d399' : 'var(--mw-t4)' }};box-shadow:{{ $wh->is_active ? '0 0 6px rgba(52,211,153,.5)' : 'none' }}"></div>
|
||||
<div style="min-width:0">
|
||||
<div style="font-size:12.5px;font-weight:500;color:var(--mw-t1);overflow:hidden;text-overflow:ellipsis;white-space:nowrap">
|
||||
{{ $wh->name }}
|
||||
<span class="mw-whl-status-sm">
|
||||
@if($wh->is_active)
|
||||
<span class="mbx-badge-ok" style="font-size:9px;margin-left:4px">Aktiv</span>
|
||||
@else
|
||||
<span class="mbx-badge-warn" style="font-size:9px;margin-left:4px">Pausiert</span>
|
||||
@endif
|
||||
</span>
|
||||
<span class="mw-whl-http-sm" style="font-family:monospace;font-size:9.5px;margin-left:4px">
|
||||
@if($wh->last_status !== null)
|
||||
@if($wh->last_status >= 200 && $wh->last_status < 300)
|
||||
<span style="color:#34d399;font-weight:600">{{ $wh->last_status }}</span>
|
||||
@else
|
||||
<span style="color:#f87171;font-weight:600">{{ $wh->last_status === 0 ? 'T/O' : $wh->last_status }}</span>
|
||||
@endif
|
||||
@endif
|
||||
</span>
|
||||
</div>
|
||||
<div style="font-family:monospace;font-size:10px;color:var(--mw-t5);overflow:hidden;text-overflow:ellipsis;white-space:nowrap">{{ $wh->url }}</div>
|
||||
</div>
|
||||
</div>
|
||||
{{-- Events --}}
|
||||
<div class="mw-whl-events" style="display:flex;flex-wrap:wrap;gap:3px;align-items:center">
|
||||
@foreach($evVisible as $ev)
|
||||
<span style="font-size:9.5px;padding:1px 5px;background:var(--mw-bg4);border:1px solid var(--mw-b2);border-radius:4px;color:var(--mw-t4);font-family:monospace;white-space:nowrap">{{ $ev }}</span>
|
||||
@endforeach
|
||||
@if($evRest > 0)
|
||||
<span style="font-size:9.5px;padding:1px 6px;background:var(--mw-bg4);border:1px solid var(--mw-b2);border-radius:4px;color:var(--mw-t4);white-space:nowrap">+{{ $evRest }}</span>
|
||||
@endif
|
||||
</div>
|
||||
{{-- Status --}}
|
||||
<div class="mw-whl-status">
|
||||
@if($wh->is_active)
|
||||
<span class="mbx-badge-ok">Aktiv</span>
|
||||
@else
|
||||
<span class="mbx-badge-warn">Pausiert</span>
|
||||
@endif
|
||||
</div>
|
||||
{{-- HTTP --}}
|
||||
<div class="mw-whl-http">
|
||||
@if($wh->last_status === null)
|
||||
<span style="font-size:11px;color:var(--mw-t4)">—</span>
|
||||
@elseif($wh->last_status >= 200 && $wh->last_status < 300)
|
||||
<span style="font-family:monospace;font-size:11px;color:#34d399;font-weight:600">{{ $wh->last_status }}</span>
|
||||
@elseif($wh->last_status === 0)
|
||||
<span style="font-family:monospace;font-size:11px;color:#f87171;font-weight:600">Timeout</span>
|
||||
@else
|
||||
<span style="font-family:monospace;font-size:11px;color:#f87171;font-weight:600">{{ $wh->last_status }}</span>
|
||||
@endif
|
||||
</div>
|
||||
{{-- Actions --}}
|
||||
<div class="mw-whl-actions">
|
||||
<div class="mbx-actions">
|
||||
<button wire:click="$dispatch('openModal',{component:'ui.system.modal.webhook-edit-modal',arguments:{webhookId:{{ $wh->id }}}})"
|
||||
class="mbx-act-btn" title="Bearbeiten">
|
||||
<svg width="12" height="12" viewBox="0 0 13 13" fill="none"><path d="M9 2l2 2-7 7H2V9l7-7Z" stroke="currentColor" stroke-width="1.2" stroke-linejoin="round"/></svg>
|
||||
</button>
|
||||
<button wire:click="toggleActive({{ $wh->id }})"
|
||||
class="mbx-act-btn" title="{{ $wh->is_active ? 'Pausieren' : 'Aktivieren' }}">
|
||||
@if($wh->is_active)
|
||||
<svg width="12" height="12" viewBox="0 0 13 13" fill="none"><rect x="3" y="2" width="2.5" height="9" rx="1" stroke="currentColor" stroke-width="1.2"/><rect x="7.5" y="2" width="2.5" height="9" rx="1" stroke="currentColor" stroke-width="1.2"/></svg>
|
||||
@else
|
||||
<svg width="12" height="12" viewBox="0 0 13 13" fill="none"><path d="M4 2.5l7 4-7 4V2.5Z" stroke="currentColor" stroke-width="1.2" stroke-linejoin="round"/></svg>
|
||||
@endif
|
||||
</button>
|
||||
<button wire:click="$dispatch('openModal',{component:'ui.system.modal.webhook-delete-modal',arguments:{webhookId:{{ $wh->id }}}})"
|
||||
class="mbx-act-btn mbx-act-danger" title="Löschen">
|
||||
<svg width="12" height="12" viewBox="0 0 13 13" fill="none"><path d="M2 3h9M5 3V2h3v1M3.5 3l.5 8h5l.5-8" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
|
||||
@endif
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -28,14 +28,31 @@
|
|||
|
||||
<span class="hidden"></span>
|
||||
|
||||
<div x-show="show && showActiveComponent"
|
||||
x-bind:class="modalWidth"
|
||||
id="modal-container"
|
||||
x-trap.noscroll.inert="show && showActiveComponent"
|
||||
aria-modal="true"
|
||||
class="mw-modal-box inline-block w-full sm:max-w-md relative z-10">
|
||||
{{-- <div x-show="show && showActiveComponent"--}}
|
||||
{{-- x-bind:class="modalWidth"--}}
|
||||
{{-- id="modal-container"--}}
|
||||
{{-- x-trap.noscroll.inert="show && showActiveComponent"--}}
|
||||
{{-- aria-modal="true"--}}
|
||||
{{-- class="mw-modal-box inline-block w-full sm:max-w-md relative z-10">--}}
|
||||
|
||||
<div class="mw-modal-inner">
|
||||
<span class="hidden"></span>
|
||||
|
||||
<div
|
||||
x-show="show && showActiveComponent"
|
||||
x-transition:enter="ease-out duration-300"
|
||||
x-transition:enter-start="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||
x-transition:enter-end="opacity-100 translate-y-0 sm:scale-100"
|
||||
x-transition:leave="ease-in duration-200"
|
||||
x-transition:leave-start="opacity-100 translate-y-0 sm:scale-100"
|
||||
x-transition:leave-end="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||
x-bind:class="modalWidth"
|
||||
class="mw-modal-box relative inline-block w-full align-bottom rounded-lg text-left shadow-xl transform transition-all sm:my-8 sm:align-middle sm:w-full"
|
||||
id="modal-container"
|
||||
x-trap.inert="show && showActiveComponent"
|
||||
aria-modal="true"
|
||||
>
|
||||
|
||||
<div class="mw-modal-inner">
|
||||
@forelse($components as $id => $component)
|
||||
<div x-show.immediate="activeComponent == '{{ $id }}'"
|
||||
x-ref="{{ $id }}"
|
||||
|
|
|
|||
Loading…
Reference in New Issue