307 lines
9.9 KiB
Markdown
307 lines
9.9 KiB
Markdown
# Clusev — STRICT RULES
|
|
|
|
> **Non-negotiable.** Generated from `handoff.md` §4. These are the user's explicit,
|
|
> mandatory rules. If any instruction (mine, yours, or a future agent's) conflicts with a
|
|
> rule here, **STOP and ask** — do not "work around" it. Keep this file open while coding.
|
|
|
|
---
|
|
|
|
## Quick gate — run through this before every commit
|
|
|
|
- [ ] **R1/R2** — Page is a **full-page, class-based Livewire component** mapped directly in `routes/web.php`.
|
|
- [ ] **R2** — Livewire **class** in `app/Livewire/…` **and** a matching **separate Blade view** in `resources/views/livewire/…`. **No Volt.**
|
|
- [ ] **R3** — No hard-coded hex/rgb in markup — only `@theme` token utilities (`bg-surface`, `text-accent`, …).
|
|
- [ ] **R4** — No inline `style="…"` **except** a progress bar's `width`.
|
|
- [ ] **R5** — Destructive/confirm action runs through a **wire-elements/modal**, never Alpine `confirm()`.
|
|
- [ ] **R6** — Files placed exactly per the §5 folder map.
|
|
- [ ] **R7** — Verified at **375 / 768 / 1280 px**; touch targets ≥ 44px.
|
|
- [ ] **R8** — Every PHP/Composer/Node/artisan command ran **inside the container**.
|
|
- [ ] **R9** — UI copy is **German**, **no emoji**; status via color/dots/pills.
|
|
- [ ] **R10** — Reused existing `@theme` tokens + Blade components (no ad-hoc colors/widgets).
|
|
- [ ] **Secrets** — `git status` shows **no** `.env*`, token, key, or home-dir dotfile staged.
|
|
|
|
---
|
|
|
|
## R1 — Pages are full-page Livewire components, mapped directly as routes
|
|
|
|
**Why.** One page = one component = one route. No controller indirection, no orphan Blade views.
|
|
Keeps routing, state, and view co-located and predictable.
|
|
|
|
✅ **Correct**
|
|
```php
|
|
// routes/web.php
|
|
use App\Livewire\Dashboard;
|
|
use App\Livewire\Servers\Show;
|
|
|
|
Route::get('/', Dashboard::class)->name('dashboard');
|
|
Route::get('/servers/{server}', Show::class)->name('servers.show');
|
|
```
|
|
|
|
❌ **Forbidden**
|
|
```php
|
|
// Controller returning a Blade view for a PAGE
|
|
Route::get('/', [DashboardController::class, 'index']);
|
|
|
|
class DashboardController {
|
|
public function index() {
|
|
return view('dashboard'); // ❌ pages do not belong to controllers
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## R2 — Class-based Livewire only. Volt is FORBIDDEN.
|
|
|
|
**Why.** Class + separate view is explicit, testable, and conventional. Volt single-file
|
|
components hide logic in the view and are banned for this project.
|
|
|
|
✅ **Correct** — `php artisan make:livewire Servers/Show` (creates class **and** view)
|
|
```php
|
|
// app/Livewire/Servers/Show.php
|
|
namespace App\Livewire\Servers;
|
|
|
|
use App\Models\Server;
|
|
use Livewire\Component;
|
|
|
|
class Show extends Component
|
|
{
|
|
public Server $server;
|
|
|
|
public function render()
|
|
{
|
|
return view('livewire.servers.show'); // ← separate Blade file
|
|
}
|
|
}
|
|
```
|
|
```blade
|
|
{{-- resources/views/livewire/servers/show.blade.php --}}
|
|
<div>{{ $server->hostname }}</div>
|
|
```
|
|
|
|
❌ **Forbidden**
|
|
```blade
|
|
{{-- Volt single-file component — NEVER. Do not run `php artisan make:volt`. --}}
|
|
<?php
|
|
use function Livewire\Volt\{state};
|
|
state(['count' => 0]);
|
|
?>
|
|
<div>{{ $count }}</div>
|
|
```
|
|
|
|
---
|
|
|
|
## R3 — All colors / design tokens live in `resources/css/app.css` under `@theme`
|
|
|
|
**Why.** Single source of truth for the visual system (Tailwind v4 CSS-first, no
|
|
`tailwind.config.js`). Markup references **tokens**, never raw colors — so a token change
|
|
restyles the whole app.
|
|
|
|
✅ **Correct**
|
|
```css
|
|
/* resources/css/app.css */
|
|
@import "tailwindcss";
|
|
@theme {
|
|
--color-surface: #0F1318;
|
|
--color-accent: #FF6B2C;
|
|
--color-online: #35D07F;
|
|
--color-line: rgba(255,255,255,.10);
|
|
}
|
|
```
|
|
```blade
|
|
<div class="bg-surface border border-line">
|
|
<span class="text-accent">CPU</span>
|
|
<span class="text-online">online</span>
|
|
<div class="bg-accent/10">soft accent tint</div> {{-- opacity modifier, not a separate token --}}
|
|
</div>
|
|
```
|
|
|
|
❌ **Forbidden**
|
|
```blade
|
|
<div class="bg-[#0F1318] border-[#ffffff1a]" style="color:#FF6B2C"> {{-- ❌ raw hex in markup --}}
|
|
```
|
|
|
|
---
|
|
|
|
## R4 — Inline styles are FORBIDDEN (one exception)
|
|
|
|
**Why.** Inline styles bypass the token system and Tailwind. The **only** legitimate dynamic
|
|
value that can't be a utility class is a progress bar's percentage width.
|
|
|
|
✅ **Correct** — the ONE allowed inline style
|
|
```blade
|
|
<div class="h-1.5 w-full rounded-full bg-line">
|
|
<div class="h-full rounded-full bg-accent" style="width: {{ $pct }}%"></div>
|
|
</div>
|
|
```
|
|
|
|
❌ **Forbidden**
|
|
```blade
|
|
<div style="margin-top:12px; display:flex; color:#FF6B2C"> {{-- ❌ use utilities: mt-3 flex text-accent --}}
|
|
```
|
|
|
|
---
|
|
|
|
## R5 — Destructive / confirm actions use a Modal, never an Alpine dialog
|
|
|
|
**Why.** Destructive actions (delete server, remove SSH key, stop a service) must be
|
|
deliberate, styled, and auditable. Use **wire-elements/modal** components — never native
|
|
`confirm()` or an `x-data` popup.
|
|
|
|
✅ **Correct**
|
|
```blade
|
|
{{-- trigger: open the modal component (exact payload per installed wire-elements/modal version) --}}
|
|
<button type="button"
|
|
wire:click="$dispatch('openModal', { component: 'modals.confirm-delete', arguments: { server: {{ $server->id }} } })"
|
|
class="rounded-md px-3 py-2 text-offline hover:bg-offline/10">
|
|
Löschen
|
|
</button>
|
|
```
|
|
```php
|
|
// app/Livewire/Modals/ConfirmDelete.php (a real Livewire modal class)
|
|
namespace App\Livewire\Modals;
|
|
|
|
use App\Models\Server;
|
|
use LivewireUI\Modal\ModalComponent; // wire-elements/modal base class
|
|
|
|
class ConfirmDelete extends ModalComponent
|
|
{
|
|
public Server $server;
|
|
|
|
public function delete(): void
|
|
{
|
|
$this->server->delete(); // audit-logged in the domain layer
|
|
$this->closeModalWithEvents([/* refresh listeners */]);
|
|
}
|
|
|
|
public function render() { return view('livewire.modals.confirm-delete'); }
|
|
}
|
|
```
|
|
|
|
❌ **Forbidden**
|
|
```blade
|
|
<button x-data @click="if (confirm('Wirklich löschen?')) $wire.delete()">Löschen</button>
|
|
{{-- ❌ no native confirm(), no Alpine confirm popups for destructive actions --}}
|
|
```
|
|
|
|
---
|
|
|
|
## R6 — Clean, conventional folder structure
|
|
|
|
**Why.** Predictable structure = fast ramp-up, no duplication, no "where does this go?".
|
|
Follow the §5 map in `CLAUDE.md` exactly.
|
|
|
|
✅ **Correct** — component, view, model, SSH wrapper each in their canonical place
|
|
```
|
|
app/Livewire/Servers/Show.php
|
|
resources/views/livewire/servers/show.blade.php
|
|
app/Models/Server.php
|
|
app/Support/Ssh/SshClient.php
|
|
```
|
|
|
|
❌ **Forbidden**
|
|
```
|
|
app/Http/Controllers/ServerController.php {{-- ❌ pages aren't controllers (R1) --}}
|
|
app/Livewire/show.blade.php {{-- ❌ view in the class namespace --}}
|
|
app/ServerSshHelperStuff.php {{-- ❌ unstructured dumping ground --}}
|
|
```
|
|
|
|
---
|
|
|
|
## R7 — Fully responsive: mobile + tablet + desktop
|
|
|
|
**Why.** Operators use phones and tablets in the field. Every screen must adapt — not just shrink.
|
|
|
|
✅ **Correct**
|
|
```blade
|
|
{{-- KPI grid reflows 1 → 2 → 4; sidebar is drawer on small, fixed on large --}}
|
|
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2 xl:grid-cols-4"> … </div>
|
|
|
|
<aside class="hidden lg:block lg:w-[272px]"> … </aside> {{-- desktop sidebar --}}
|
|
<button class="lg:hidden min-h-11 min-w-11" wire:click="$toggle('navOpen')">…</button> {{-- ≥44px touch --}}
|
|
```
|
|
|
|
❌ **Forbidden**
|
|
```blade
|
|
<div class="grid grid-cols-4"> {{-- ❌ desktop-only; breaks on mobile/tablet --}}
|
|
<aside class="w-[272px]"> {{-- ❌ always-on sidebar, no drawer --}}
|
|
```
|
|
Breakpoints to test: **375** (mobile), **768** (tablet), **1280+** (desktop).
|
|
|
|
---
|
|
|
|
## R8 — Docker-first, host stays clean
|
|
|
|
**Why.** The target host has **only Docker** + a sudo user — no PHP/Node/Composer. Everything
|
|
runs in containers, in dev and prod. Run all tooling through the `app` (or a throwaway) container.
|
|
|
|
✅ **Correct**
|
|
```bash
|
|
docker compose exec app php artisan make:livewire Dashboard
|
|
docker compose exec app composer require some/package
|
|
docker compose exec app php artisan migrate
|
|
```
|
|
|
|
❌ **Forbidden**
|
|
```bash
|
|
php artisan migrate # ❌ assumes PHP on the host — there is none
|
|
composer install # ❌ host has no Composer
|
|
npm run build # ❌ run node inside the vite/app container
|
|
```
|
|
|
|
---
|
|
|
|
## R9 — German UI copy, no emoji
|
|
|
|
**Why.** Console/status-page register: terse, operational, factual. Status is communicated
|
|
by **color/dots/pills**, never emoji. Technical identifiers stay native.
|
|
|
|
✅ **Correct**
|
|
```blade
|
|
<x-status-pill status="online">Online</x-status-pill>
|
|
<span class="font-mono">nginx.service</span>
|
|
<button>Dienst neu starten</button>
|
|
```
|
|
|
|
❌ **Forbidden**
|
|
```blade
|
|
<span>🟢 Online ✅</span> {{-- ❌ emoji as status --}}
|
|
<button>Restart service</button> {{-- ❌ English UI copy --}}
|
|
```
|
|
Keep native technical tokens as-is: `nginx.service`, `systemd`, `chmod 600`, `SSH`, `2FA`.
|
|
|
|
---
|
|
|
|
## R10 — Reuse the existing design system
|
|
|
|
**Why.** The visual system (tokens + components) is already designed ("Tactical Terminal",
|
|
softened orange). Don't reinvent colors or one-off widgets — compose from the shared kit.
|
|
|
|
✅ **Correct** — use the Blade component kit + tokens
|
|
```blade
|
|
<x-panel title="Auslastung">
|
|
<x-kpi label="CPU" :value="$cpu" unit="%" />
|
|
<x-status-dot status="warning" />
|
|
</x-panel>
|
|
```
|
|
|
|
❌ **Forbidden**
|
|
```blade
|
|
<div class="rounded-xl bg-[#12161c] p-4 shadow"> {{-- ❌ ad-hoc panel, raw hex --}}
|
|
<div style="color:#ffa500">CPU</div> {{-- ❌ new orange, inline style --}}
|
|
</div>
|
|
```
|
|
|
|
---
|
|
|
|
## Appendix — Secret hygiene (this repo is rooted in `$HOME`)
|
|
|
|
This repository lives in the dev user's home directory, which also holds credentials
|
|
(`.env.gitea`, `.claude.json`, SSH keys). To prevent leaking secrets to the Gitea remote:
|
|
|
|
- **Never** `git add -A` / `git add .`. Stage explicit project paths only.
|
|
- `.env*` (incl. `.env.gitea`), keys, and home-dir dotfiles are in `.gitignore` — keep it that way.
|
|
- The Gitea `GIT_ACCESS_TOKEN` lives in `.env.gitea` (untracked). It must later move into the
|
|
app `.env`. Read it only at push time; never echo it, never commit it.
|
|
- Before any `git push`, eyeball `git status` and the staged diff for stray secrets.
|