11 KiB
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.Stack note (2026-06-11): built on Laravel 13 (handoff said 12; updated by user decision). The rules below are version-agnostic.
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 inresources/views/livewire/…. No Volt. - R3 — No hard-coded hex/rgb in markup — only
@themetoken utilities (bg-surface,text-accent, …). - R4 — No inline
style="…"except a progress bar'swidth. - 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
@themetokens + Blade components (no ad-hoc colors/widgets). - R11 — Records exposed in URLs are addressed by UUID, never the integer PK.
- Secrets —
git statusshows 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
// 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
// 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)
// 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
}
}
{{-- resources/views/livewire/servers/show.blade.php --}}
<div>{{ $server->hostname }}</div>
❌ Forbidden
{{-- 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
/* resources/css/app.css */
@import "tailwindcss";
@theme {
--color-surface: #0F1318;
--color-accent: #FF6B2C;
--color-online: #35D07F;
--color-line: rgba(255,255,255,.10);
}
<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
<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
<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
<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
{{-- 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>
// 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
<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
{{-- 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
<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
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
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
<x-status-pill status="online">Online</x-status-pill>
<span class="font-mono">nginx.service</span>
<button>Dienst neu starten</button>
❌ Forbidden
<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
<x-panel title="Auslastung">
<x-kpi label="CPU" :value="$cpu" unit="%" />
<x-status-dot status="warning" />
</x-panel>
❌ Forbidden
<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>
R11 — Public URLs use UUIDs, never the numeric primary key
Why. Auto-increment IDs in URLs are enumerable and leak record counts/ordering. Anything addressable in a route is referenced by an unguessable UUID; the integer PK stays internal (relations, performance).
✅ Correct
// app/Models/Server.php — keep the bigint id internally, expose a uuid in URLs
use Illuminate\Support\Str;
class Server extends Model
{
protected static function booted(): void
{
static::creating(fn (self $m) => $m->uuid ??= (string) Str::uuid());
}
public function getRouteKeyName(): string
{
return 'uuid'; // {server} in routes resolves by uuid, not the bigint id
}
}
// migration: $table->uuid('uuid')->unique();
// routes/web.php — {server} binds by uuid
Route::get('/servers/{server}', \App\Livewire\Servers\Show::class)->name('servers.show');
// → /servers/9f3c0b1a-7e21-... (never /servers/42)
❌ Forbidden
Route::get('/servers/{id}', ...); // integer PK in the URL — enumerable
route('servers.show', $server->id); // exposes the auto-increment id
Appendix — Secret hygiene
The project lives in /home/nexxo/clusev (its own git repo). The Gitea push token lives in
../.env.gitea (in the parent home dir, untracked) and must later move into the app .env.
.env*(incl..env.gitea), keys, and local state are in.gitignore— keep it that way.- Never commit or echo the
GIT_ACCESS_TOKEN; read it only at push time. - Before any
git push, eyeballgit statusand the staged diff for stray secrets.