chore: project bootstrap — rules.md, CLAUDE.md, .gitignore

Generate the two governing docs from handoff.md:
- rules.md: STRICT RULES R1-R10, each with rationale + correct/forbidden example
- CLAUDE.md: product, stack, folder map, commands, conventions, before-you-code checklist
Add a $HOME-rooted .gitignore that blocks all secrets and home-dir dotfiles
(explicit-add-only workflow). Track handoff.md + kickoff-prompt.md as context.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
feat/v1-foundation
boban 2026-06-11 23:12:58 +02:00
commit 3e5a1ff81c
5 changed files with 953 additions and 0 deletions

64
.gitignore vendored Normal file
View File

@ -0,0 +1,64 @@
# ─────────────────────────────────────────────────────────────────────────────
# Clusev .gitignore
# This repo is ROOTED IN THE DEV USER'S $HOME. We therefore ignore all home-dir
# dotfiles/tooling and ALL secrets by default.
# HARD RULE: never `git add -A` / `git add .` here — stage project paths
# explicitly. See rules.md → "Secret hygiene".
# ─────────────────────────────────────────────────────────────────────────────
# ── Secrets (NEVER commit) ───────────────────────────────────────────────────
.env
.env.*
!.env.example
.env.gitea
*.pem
*.key
id_rsa*
id_ed25519*
.git-credentials
.netrc
auth.json
# ── Home-dir / tooling noise (repo is rooted in $HOME) ───────────────────────
.bash_logout
.bash_history
.bashrc
.profile
.wget-hsts
.cache/
.npm/
.dotnet/
.claude/
.claude.json
.vscode-server/
.config/
.local/
.ssh/
.gnupg/
.docker/
.composer/
# ── Laravel ──────────────────────────────────────────────────────────────────
/vendor
/node_modules
/public/build
/public/hot
/public/storage
/storage/*.key
/storage/pail
/bootstrap/ssr
/.phpunit.cache
.phpunit.result.cache
Homestead.json
Homestead.yaml
npm-debug.log
yarn-error.log
# ── Editors / OS ─────────────────────────────────────────────────────────────
/.idea
/.vscode
/.fleet
/.nova
/.zed
.DS_Store
Thumbs.db

236
CLAUDE.md Normal file
View File

@ -0,0 +1,236 @@
# CLAUDE.md — Clusev
Project context for any agent/developer. **Read `rules.md` first — those rules are
non-negotiable.** This file is the fast ramp-up: what we're building, the stack, where things
live, how to run them, and the conventions. Source of truth for decisions: `handoff.md`.
---
## 1. Product
**Clusev** — a self-hosted control panel to administer a **fleet of Linux servers from one
dashboard**, connecting **agentless over SSH**. The operator sees and steers the whole fleet
(metrics, services, files, audit) in one security-first UI (2FA, full audit log).
- **Backend reality:** Laravel is the **control-plane** (UI + API + provisioning). It does **not**
reimplement SSH/daemons — it orchestrates real servers over SSH via **phpseclib** (exec + SFTP).
- **Audience:** developers, self-hosters, sysadmins.
- **Differentiator:** multi-server fleet management with a modern, polished UI. **Multi-server is
free and never paywalled.** Paid = Team/Enterprise (SSO/LDAP, RBAC, audit export, alerting,
backups) + optional managed control-plane. **License:** AGPL core + commercial Pro modules —
architect open-core from day one (clean core, Pro as separate modules/flags).
### v1 scope (build this first — do NOT boil the ocean)
- **Foundation (once):** auth + **2FA**, **audit log**, encrypted **SSH-credential vault**,
**SSH layer** (exec + SFTP via phpseclib), **Reverb** realtime channel, **queue** workers,
**multi-server switcher**.
- **Features:** (1) **Dashboard / live metrics**, (2) **systemd services** (list + start/stop/
restart + logs), (3) **SFTP file manager**, plus the **Server-Details** page (identity,
resource gauges, specs, volumes, interfaces, security hardening, SSH keys).
### Deferred (keep OUT of v1)
- **Web terminal** (xterm.js + ws↔SSH sidecar — PHP can't hold an interactive PTY).
- **Package management** (apt/dnf), firewall / users / cron, app store, push-metrics agent.
---
## 2. Tech stack (hard requirements)
| Layer | Choice |
|---|---|
| Framework | **Laravel 12** |
| UI / interactivity | **Livewire v3** — class-based, **NOT Volt** |
| CSS | **Tailwind v4** (CSS-first, `@theme` in `app.css`, **no `tailwind.config.js`**) |
| Build | **Vite** |
| Modals | **wire-elements/modal** |
| Realtime | **Laravel Reverb** + Echo (live metrics, broadcasts) |
| Queue / cache | **Redis** |
| DB | **MariaDB** *(confirmed)* |
| SSH | **phpseclib** (exec + SFTP) |
| Charts | JS lib (ApexCharts / Chart.js / uPlot) as an **Alpine island** in Blade |
| Icons | **Lucide**, inline SVG via a Blade `x-icon` component |
| Fonts | **Chakra Petch** (display) · **Space Grotesk** (sans) · **JetBrains Mono** (numbers/paths) — self-host `.woff2`, no CDN in prod |
**Composer packages:** `livewire/livewire wire-elements/modal phpseclib/phpseclib laravel/reverb`.
**NPM (Tailwind v4):** `tailwindcss @tailwindcss/vite` (+ Echo + a charts lib).
---
## 3. Architecture notes
- **Pages = full-page class-based Livewire components** mapped directly in `routes/web.php`
(R1/R2). No page controllers.
- **SSH layer** (`app/Support/Ssh/`): `SshClient` (exec), `Sftp`, `CredentialVault`
(encrypted credentials). Domain services in `app/Services/` (`FleetService`, `MetricsPoller`,
`Provisioner`) orchestrate them; queued long ops go in `app/Jobs/`.
- **Realtime:** `MetricsPoller` → broadcast events (`app/Events/`, e.g. `MetricsTicked`) over
**Reverb**; the Dashboard Livewire component receives them (Echo / `wire:stream`) and updates
the chart island. **Mock data first, real SSH after.**
- **Open-core:** keep the core clean; Pro features behind separate modules/flags from day one.
---
## 4. Folder map (§5 — follow exactly, R6)
```
app/
Livewire/ # class-based components (full-page = routes, + nested)
Dashboard.php
Servers/{Index.php, Show.php} # Show = Server-Details page
Services/Index.php
Files/Index.php
Audit/Index.php
Auth/{Login.php, TwoFactor.php}
Modals/ # wire-elements/modal components (ConfirmDelete, ServerForm, …)
Concerns/ # shared traits (e.g. WithFleetContext)
Models/ # Server, AuditEvent, User, …
Support/Ssh/ # phpseclib wrappers: SshClient, Sftp, CredentialVault
Services/ # FleetService, MetricsPoller, Provisioner
Events/ # broadcast events (MetricsTicked, …)
Jobs/ # queued ops
resources/
views/
livewire/ # mirrors app/Livewire (kebab-case)
dashboard.blade.php
servers/{index,show}.blade.php
services/index.blade.php
files/index.blade.php
audit/index.blade.php
modals/…
components/ # Blade UI: panel, kpi, status-pill, status-dot, badge,
… # sidebar, topbar, server-item, nav-item, icon, ring
layouts/app.blade.php
css/app.css # @import "tailwindcss"; + @theme { tokens } + base layer
js/app.js # Echo/Reverb bootstrap, Alpine islands (charts; xterm later)
routes/web.php # full-page Livewire routes
config/livewire.php # class_namespace=App\Livewire, view_path=views/livewire
docker/ # Dockerfile bits, php-fpm/nginx config, entrypoint
docker-compose.yml # DEV (build, bind-mount, Vite)
docker-compose.prod.yml # PROD (image from GHCR, no mount)
.dockerignore
handoff.md rules.md CLAUDE.md
```
---
## 5. Design system → Tailwind v4 `@theme`
Direction: **"Tactical Terminal"** — dark graphite, **signal-orange** brand (softened on
selection/active tints), **cyan** counter-accent, ops status triad, **mono for every
number/IP/path**. All tokens live in `resources/css/app.css` (R3). Token groups:
- **Surfaces:** `void, base, surface, raised, inset``bg-surface`, `bg-raised`, …
- **Brand:** `accent, accent-bright, accent-deep, accent-text``text-accent`, `bg-accent/10`
(use **opacity utilities** for tints, e.g. `bg-accent/10`, `border-accent/25` — no `--accent-dim`).
- **Counter-accent:** `cyan, cyan-bright`.
- **Status:** `online (#35D07F)`, `warning (#E8B931)`, `offline (#FF5247)``text-online`, …
- **Text ramp:** `ink, ink-2, ink-3, ink-4``text-ink`, `text-ink-2`, …
- **Borders (hairline):** `line, line-soft, line-strong``border-line`, …
- **Fonts:** `--font-display` (Chakra Petch), `--font-sans` (Space Grotesk), `--font-mono`
(JetBrains Mono) → `font-display`, `font-mono`.
- **Radii:** `--radius-xs/sm/md/lg``rounded-sm`, `rounded-lg`.
> Full token values are in `handoff.md` §6 — port them verbatim into `app.css`. Selection
> (`::selection`) and active nav/server tints use `bg-accent/25` and `bg-accent/10` + `border-accent/25`
> (keep subtle). **`../bastion` mockup is NOT present on this machine** → rebuild from §6 tokens +
> the §11 component list; pull Lucide SVG paths directly from Lucide.
**Blade components to build:** `x-panel`, `x-kpi`, `x-status-pill`, `x-status-dot`, `x-badge`,
`x-icon` (Lucide), `x-ring`, `x-server-item`, `x-nav-item`, plus `sidebar` + `topbar` partials.
---
## 6. Conventions
- **UI copy: German**, terse/operational, **no emoji** (status = color/dots/pills). Native
technical tokens stay as-is (`nginx.service`, `chmod 600`, `SSH`, `2FA`). (R9)
- **Colors:** only `@theme` token utilities in markup — never raw hex/rgb. (R3)
- **Inline styles:** forbidden except a progress bar's `width`. (R4)
- **Naming:** Livewire class `App\Livewire\Servers\Show` ↔ view `livewire.servers.show`
(kebab, mirrored path). Components in `app/Livewire/…`, views in `resources/views/livewire/…`.
- **Pages:** full-page Livewire components as routes — no page controllers. (R1/R2)
- **Destructive actions:** wire-elements/modal only — no `confirm()`/Alpine popups. (R5)
- **Responsive:** every screen at **375 / 768 / 1280+**; sidebar → drawer on small; KPI grid
4→2→1; tables scroll/stack; touch targets ≥ 44px. (R7)
- **Docs language:** these meta-docs are in English; **UI strings are German**.
---
## 7. Commands (everything in containers — R8)
> Dev stack services: `app` (php-fpm + nginx/FrankenPHP), `vite`, `reverb`, `queue`,
> `mariadb`, `redis`. Vite HMR is configured for remote access (`host=0.0.0.0`,
> `hmr.host=10.10.90.136`).
```bash
# ── Stack lifecycle (DEV) ────────────────────────────────────────────────
docker compose up -d # start the dev stack
docker compose logs -f app # tail app logs
docker compose down # stop
# ── Run tooling INSIDE the container ─────────────────────────────────────
docker compose exec app php artisan migrate
docker compose exec app php artisan make:livewire Servers/Show # class + view (never make:volt)
docker compose exec app composer require some/package
docker compose exec app php artisan tinker
# ── One-time bootstrap (host has no PHP/Composer/Node) ───────────────────
# run in a throwaway container or the app image:
# composer create-project laravel/laravel .
# composer require livewire/livewire wire-elements/modal phpseclib/phpseclib laravel/reverb
# npm install -D tailwindcss @tailwindcss/vite
# php artisan livewire:publish --config # class-based, view_path set
# ── Realtime / queue (run as their own services; manual run if needed) ───
docker compose exec app php artisan reverb:start
docker compose exec app php artisan queue:work
# ── Prod deploy (plain Docker Compose — no Portainer) ────────────────────
docker compose -f docker-compose.prod.yml pull
docker compose -f docker-compose.prod.yml up -d
docker compose -f docker-compose.prod.yml exec app php artisan migrate --force
```
---
## 8. Infrastructure & deploy (§8)
- **Target VM:** Debian 13, **`10.10.90.136`**, **only Docker** + a sudo user. Everything in
containers. **No Portainer, no external orchestrator.** Dev happens on this same VM (prod-parity).
- **`docker-compose.yml` (DEV):** `build:`, **bind-mounts** source, runs Vite dev server with
remote HMR (`server.host=0.0.0.0`, `hmr.host=10.10.90.136`).
- **`docker-compose.prod.yml` (PROD):** `image: ghcr.io/OWNER/clusev:<tag>` *(GHCR namespace is a
**placeholder `OWNER`** — set via `.env` before the first prod push)*, **no bind-mount**, assets
baked into the image, no Vite. Reverse proxy + TLS (Caddy/Traefik) is just another service.
- **Deploy:** wrap pull+up+migrate in `deploy.sh` (or a CI job that SSHes in). Image flow:
push → CI builds + pushes to **GHCR**`deploy.sh` pulls onto the VM. Bootstrap before CI:
build on the VM. Use `docker buildx` (multi-arch) only if any dev happens on arm64; prod is amd64.
---
## 9. Repo & workflow
- **Project root = `/home/nexxo`** (the dev user's `$HOME`; matches §5 placement of docs).
- **Remote:** Gitea `git.bave.dev/boban/clusev.git`. Credentials live in **`.env.gitea`**
(untracked) — the `GIT_ACCESS_TOKEN` must later move into the app `.env`.
- **Work on a feature branch, not `main`.** Commit in sensible steps.
- **Secret hygiene (critical — repo is in `$HOME`):** never `git add -A`/`.`; stage explicit
project paths; `.env*`, keys, and home-dir dotfiles are gitignored. See `rules.md` → Appendix.
- **Verify before "done":** container builds, page renders, no console errors. Test the 3 breakpoints.
---
## 10. Before you code — checklist
1. **Read `rules.md`.** R1R10 are non-negotiable; on any conflict, **stop and ask**.
2. New page? → full-page **class-based** Livewire component + route in `web.php` (R1/R2). Never Volt.
3. Need a color/spacing? → an **`@theme` token utility** (R3). No raw hex, no inline style except
progress `width` (R4).
4. Destructive/confirm? → **wire-elements/modal** (R5).
5. Place files per the **§4 folder map** (R6). Class ↔ mirrored kebab view.
6. Build **responsive**: 375 / 768 / 1280; ≥44px touch targets (R7).
7. Run all tooling **inside the container** (R8).
8. UI copy **German, no emoji**; status via color/dots/pills (R9).
9. **Reuse** the token set + Blade component kit (R10).
10. **Verify** (build, render, no console errors, 3 breakpoints), then `git status` for stray
secrets, commit on the feature branch.

302
handoff.md Normal file
View File

@ -0,0 +1,302 @@
# Clusev — Project Handoff
> **Purpose of this file.** This is the single source of truth for the project's
> rules and decisions. In the next session, generate two files from it:
> 1. **`rules.md`** — concise, enforceable coding rules (the "STRICT RULES" section, expanded with examples).
> 2. **`CLAUDE.md`** — project context, stack, commands, conventions, folder map (so any agent ramps up fast).
>
> Do not lose any rule below. If something is ambiguous, ask the user before coding.
---
## 1. Product
**Clusev** is a **self-hosted control panel to administer a fleet of Linux servers from one
dashboard.** It connects **agentless over SSH**. The operator sees and steers the whole fleet
in one UI — metrics, services, files, audit — security-first (2FA, full audit log).
- **Audience:** developers, self-hosters, sysadmins. End-beneficiaries are broad.
- **Differentiator:** multi-server fleet management with a modern, polished UI (vs Cockpit/aaPanel single-host + dated UX).
- **Backend reality:** Laravel is the **control-plane** (UI + API + provisioning). It does NOT
reimplement SSH/daemons — it orchestrates real servers over SSH (phpseclib for exec + SFTP).
### v1 scope (build this first — do NOT boil the ocean)
Foundation (build once): auth + **2FA**, **audit log**, encrypted SSH-credential vault,
SSH layer (exec + SFTP via phpseclib), Reverb realtime channel, queue workers, multi-server switcher.
Features: **(1) Dashboard/live metrics**, **(2) systemd services** (list + start/stop/restart + logs),
**(3) SFTP file manager**. Plus the **Server-Details** page (identity, resource gauges, specs,
volumes, interfaces, security hardening, SSH keys).
### Deferred (later versions — keep out of v1)
- **Web terminal** (xterm.js Alpine island + small ws↔SSH sidecar) — PHP can't hold an interactive PTY.
- **Package management** (apt/dnf) — write-risk + distro abstraction.
- Firewall / users / cron, one-click app store, optional push-metrics agent.
### Monetization & license (decided)
- **Multi-server is FREE** (it's the differentiator — never paywall it).
- **Paid = Team/Enterprise** (SSO/LDAP, granular RBAC, audit retention/export, alerting,
auto-backups, support) + optional **Managed/Hosted** control-plane.
- **License:** AGPL for the core (protects against SaaS rip-offs, stays OSS) + commercial
license for Pro modules. Architect open-core from day 1 (clean core, Pro as separate modules/flags).
---
## 2. Name & branding
- **Project name:** `Clusev` (all handles/domains free: `clusev.*`, `get…`, `use…`, `.sh`).
- **No `.com` required.** GitHub repo + README + screenshots is the homepage for now. No marketing
website yet (comes later, with the Pro/Managed tier).
- **Design origin:** the visual system was prototyped under the codename **"BASTION"** (see §11).
Rename the brandmark to **Clusev** (the accented letter in the wordmark becomes the brand-orange accent).
- **UI copy language: German.** Terse, operational, factual (console/status-page register).
No emoji. Status = color/dots/pills, never emoji. Technical identifiers stay native
(`nginx.service`, `systemd`, `chmod 600`, `SSH`, `2FA`).
---
## 3. Tech stack (hard requirements)
| Layer | Choice |
|---|---|
| Framework | **Laravel 12** |
| UI/interactivity | **Livewire v3** (class-based — **NOT Volt**) |
| CSS | **Tailwind v4** (CSS-first, `@theme` in `app.css` — no `tailwind.config.js`) |
| Build | **Vite** |
| Modals | **wire-elements/modal** (`composer require wire-elements/modal`) |
| Realtime | **Laravel Reverb** + Echo (live metrics, broadcasts) |
| Queue/cache | **Redis** |
| DB | **MariaDB** (default; Postgres acceptable) |
| SSH | **phpseclib** (exec + SFTP) |
| Charts | JS lib (ApexCharts / Chart.js / uPlot) as an **Alpine island** in Blade |
| Icons | **Lucide**, rendered inline as SVG (Blade component) |
| Fonts | Chakra Petch (display) · Space Grotesk (sans) · JetBrains Mono (numbers/paths). Self-host the `.woff2` (don't rely on CDN in prod). |
---
## 4. STRICT RULES (non-negotiable)
> These are the user's explicit, mandatory rules. Expand each into `rules.md` with a short
> rationale and a ✅/❌ code example.
- **R1 — Pages are full-page Livewire components, mapped directly as routes.**
Every page = a class-based Livewire component referenced in `routes/web.php`
(`Route::get('/x', SomeComponent::class)`). **No controllers returning Blade views for pages.**
The view belongs to the Livewire component, not to a controller.
- **R2 — Class-based Livewire only. Volt is FORBIDDEN.**
Each component = a PHP class in `app/Livewire/...` **plus a separate** Blade view in
`resources/views/livewire/...`. Never single-file Volt. Keep class and view as separate files.
Use `php artisan make:livewire ...` (class). Never `make:volt`.
- **R3 — All colors/design tokens live in `resources/css/app.css` under `@theme`.**
Tailwind v4 placeholders like `--color-accent`, `--color-surface`, used in markup as
`text-accent`, `bg-surface`, `bg-accent/50` (opacity modifier), `border-line`, etc.
No hard-coded hex in markup — only theme utilities.
- **R4 — Inline styles are FORBIDDEN.**
The **only** allowed inline style is a **progress bar's width** (to show a dynamic percentage,
e.g. `style="width: {{ $pct }}%"`). Everything else = Tailwind utility classes.
- **R5 — Destructive/confirm actions use a Modal, never an Alpine dialog.**
No Alpine `confirm()`/`x-data` confirm popups for delete/destructive actions.
Always a **wire-elements/modal** component (e.g. `ConfirmDelete`). Open via the package API.
- **R6 — Clean, conventional folder structure** (see §5). Organized, predictable, no mess.
- **R7 — Fully responsive: mobile + tablet + desktop.**
Every screen adapts. Sidebar → drawer/off-canvas on mobile; dense tables → stacked cards or
horizontal scroll on small screens; KPI grid reflows; touch targets ≥ 44px. Test all 3 breakpoints.
- **R8 — Docker-first, host stays clean.**
The target server has **only Docker installed** + a sudo user — nothing else (no PHP/Node/Composer
on the host). Everything runs in containers. Dev and prod both via Docker, deployed as a plain
Docker Compose stack — no Portainer, no external orchestrator (see §8).
- **R9 — German UI copy, no emoji** (see §2).
- **R10 — Reuse the existing design system** (tokens + components from the BASTION mockup,
orange accent already softened). See §6 and §11.
---
## 5. Folder structure
```
app/
Livewire/ # class-based components (full-page = routes, + nested)
Dashboard.php
Servers/
Index.php Show.php # Show = the Server-Details page
Services/Index.php
Files/Index.php
Audit/Index.php
Auth/{Login.php, TwoFactor.php}
Modals/ # wire-elements/modal components (ConfirmDelete, ServerForm, ...)
Concerns/ # shared traits (e.g. WithFleetContext)
Models/ # Server, AuditEvent, User, ...
Support/
Ssh/ # phpseclib wrappers: SshClient, Sftp, CredentialVault
Services/ # domain services: FleetService, MetricsPoller, Provisioner
Events/ # broadcast events (MetricsTicked, ...)
Jobs/ # queued ops (long installs/upgrades later)
resources/
views/
livewire/ # mirrors app/Livewire (kebab-case)
dashboard.blade.php
servers/{index,show}.blade.php
services/index.blade.php
files/index.blade.php
audit/index.blade.php
modals/...
components/ # Blade UI components: panel, kpi, status-pill, status-dot,
... # badge, sidebar, topbar, server-item, nav-item, icon, ring
layouts/app.blade.php
css/app.css # @import "tailwindcss"; + @theme { tokens } + base layer
js/app.js # Echo/Reverb bootstrap, Alpine islands (charts, xterm later)
routes/web.php # full-page Livewire routes
config/livewire.php # class_namespace=App\Livewire, view_path=views/livewire
docker/ # Dockerfile, php-fpm/nginx config, entrypoint
docker-compose.yml # DEV (build, bind-mount, Vite)
docker-compose.prod.yml # PROD (image from GHCR, no mount) — deployed via plain docker compose
.dockerignore
handoff.md rules.md CLAUDE.md
```
---
## 6. Design system → Tailwind v4 `@theme`
Visual direction: **"Tactical Terminal"** — dark graphite, **signal-orange** brand (softened on
selection/active tints), **cyan** counter-accent, ops status triad. Mono for every number/IP/path.
Port the mockup tokens (in `bastion/bastion.css`) into `resources/css/app.css`:
```css
@import "tailwindcss";
@theme {
/* surfaces */
--color-void: #06080A; --color-base: #0A0D10; --color-surface: #0F1318;
--color-raised: #141A20; --color-inset: #0B0E12;
/* brand (use opacity utilities for tints: bg-accent/10 instead of a separate --accent-dim) */
--color-accent: #FF6B2C; --color-accent-bright: #FF8A4D; --color-accent-deep: #E25617;
--color-accent-text: #FF9259;
--color-cyan: #43C1D8; --color-cyan-bright: #74D7E8;
/* status */
--color-online: #35D07F; --color-warning: #E8B931; --color-offline: #FF5247;
/* text ramp → text-ink / text-ink-2 ... */
--color-ink: #E9EEF3; --color-ink-2: #9DAAB6; --color-ink-3: #69757F; --color-ink-4: #495159;
/* hairline borders → border-line ... */
--color-line: rgba(255,255,255,.10); --color-line-soft: rgba(255,255,255,.055);
--color-line-strong: rgba(255,255,255,.17);
/* fonts (self-host the woff2) */
--font-display: "Chakra Petch", sans-serif;
--font-sans: "Space Grotesk", sans-serif;
--font-mono: "JetBrains Mono", monospace;
/* radii → rounded-* */
--radius-xs: 3px; --radius-sm: 6px; --radius-md: 8px; --radius-lg: 10px;
}
```
Usage in markup: `bg-surface`, `text-ink`, `text-accent`, `bg-accent/10`, `border-line`,
`font-mono`, `rounded-lg`, `text-online`. The old `--accent-dim`/`--accent-line` become
`bg-accent/10` and `border-accent/25`.
**Components to build as Blade components** (from the mockup markup, see §11):
`x-panel`, `x-kpi`, `x-status-pill`, `x-status-dot`, `x-badge`, `x-icon` (Lucide), `x-ring`,
`x-server-item`, `x-nav-item`, plus `sidebar` + `topbar` partials. Live metrics via Reverb events
pushed into the Livewire Dashboard component (`wire:stream` or broadcast → JS chart update).
**Selection/active orange already softened:** `::selection` uses `bg-accent/25`; active nav/server
tints use `bg-accent/10` + `border-accent/25`. Keep them subtle.
---
## 7. Responsive requirements (R7 detail)
- **Sidebar:** fixed 272px on desktop; **off-canvas drawer** (hamburger in topbar) on mobile/tablet.
- **Topbar:** on narrow widths collapse the server-meta badges + uptime (already done in mockup CSS).
- **KPI grid:** 4-up desktop → 2-up tablet → 1-up mobile.
- **Tables (services, volumes, interfaces):** horizontal scroll OR stack into cards on mobile.
- **Server-Details columns:** 2-col desktop → 1-col below ~1080px.
- Touch targets ≥ 44px; test mobile (375), tablet (768), desktop (1280+).
---
## 8. Infrastructure: Docker + deploy target (no Portainer)
**Target server (deploy + realistic dev):** Debian 13 VM, **`10.10.90.136`**, **only Docker
installed** + a sudo user. Nothing else on the host — everything runs in containers, deployed as a
plain Docker Compose stack. **No Portainer, no external orchestrator.**
**Two compose files:**
- `docker-compose.yml` (DEV): services `app` (php-fpm + nginx or FrankenPHP), `vite`, `reverb`,
`queue` (worker), `mariadb`, `redis`. Uses `build:`, **bind-mounts the source**, runs Vite dev
server. Configure Vite for remote HMR: `server.host = '0.0.0.0'`, `hmr.host = '10.10.90.136'`.
- `docker-compose.prod.yml` (PROD): `image: ghcr.io/<user>/clusev:<tag>`,
**no bind-mount**, built assets baked into the image, no Vite. Env via a `.env` file on the VM.
**Deploy workflow (plain Docker Compose — no Portainer):**
- SSH to the VM and run:
`docker compose -f docker-compose.prod.yml pull && docker compose -f docker-compose.prod.yml up -d`,
then migrate: `docker compose exec app php artisan migrate --force`.
- Wrap it in a small `deploy.sh` (or a CI job that SSHes in and runs the above).
- Image flow: push → CI builds + pushes to **GHCR**`deploy.sh` pulls the new image onto the VM.
(Bootstrap before CI exists: build on the VM with `docker compose -f docker-compose.prod.yml build`.)
- Reverse proxy + TLS (Caddy or Traefik) sits in front — also just a service in the stack.
- Build **multi-arch** (`docker buildx`) if any dev happens on Apple Silicon (arm64); the prod VM is amd64.
- *Note:* managing Docker/containers may later become a **feature of Clusev itself** — but Clusev's
own deployment never depends on any external panel.
---
## 9. Dev workflow (build in the VM)
Decision: **develop directly on the Debian 13 VM** (prod-parity, no arch mismatch, same host as
prod). Connect via **VS Code Remote-SSH** (or CLI). Everything runs in containers — the host only
has Docker. Bootstrap Laravel **inside a container** (host has no PHP/Composer/Node):
```
# one-time bootstrap (run in a throwaway php/composer container or the app image)
composer create-project laravel/laravel .
composer require livewire/livewire wire-elements/modal phpseclib/phpseclib laravel/reverb
# Tailwind v4 + Vite plugin
npm install -D tailwindcss @tailwindcss/vite
php artisan livewire:publish --config # ensure class-based, view_path set
```
Then: scaffold layout + `app.css` `@theme`, build the Blade UI components, wire the full-page
Livewire routes, hook Reverb for live metrics.
---
## 10. Next session — what to produce
1. Read this `handoff.md` end to end.
2. Generate **`rules.md`**: the §4 STRICT RULES, each expanded with a one-line rationale and a
✅correct / ❌forbidden code snippet (especially R1R5). Keep it short and enforceable.
3. Generate **`CLAUDE.md`**: product summary (§1), stack (§3), folder map (§5), key commands
(§9), conventions (German UI, tokens, responsive), and a "before you code" checklist that
references `rules.md`.
4. Then scaffold per §8/§9 (Docker-first, in the VM) and start v1 (§1 scope).
---
## 11. Reference assets (the mockup)
The working visual prototype lives in **`../bastion/`**:
- `bastion.css` — all design tokens + component CSS (source for the `@theme` mapping).
- `bastion-ui.js` — Lucide icon paths, mock data (servers/services/files/audit), metric helpers,
sidebar/topbar markup. Reuse the icon set + mock data shapes.
- `index.html` — Dashboard screen (fleet, KPIs, live chart, systemd table, files, audit).
- `details.html` — Server-Details screen (hero, resource rings, specs, volumes, interfaces, security, keys).
These are the pixel reference. Recreate them as Blade components + Livewire pages (don't copy the
prototype's internal JS structure where Livewire/Reverb fits better). Live data is mock for now;
later it comes from the SSH layer.
## 12. Open questions / TODO for the user
- Final confirm name **Clusev** + grab GitHub org + `clusev.sh` (or chosen TLD).
- DB choice confirm (MariaDB assumed).
- GHCR namespace / registry for prod images.
- Self-host the 3 webfonts (download woff2 into the repo).

45
kickoff-prompt.md Normal file
View File

@ -0,0 +1,45 @@
Lies zuerst `handoff.md` in diesem Verzeichnis KOMPLETT (oben bis unten). Das ist die
verbindliche Quelle für Produkt, Stack, Regeln, Ordnerstruktur und Deployment des Projekts
**Clusev** (self-hosted Multi-Server-Control-Panel, agentless über SSH).
Kontext dieser Maschine: Debian 13, `10.10.90.136`, **nur Docker + ein sudo-User** installiert —
sonst nichts (kein PHP/Node/Composer auf dem Host). Alles läuft in Containern. Deployment = plain
Docker Compose, kein Portainer.
Arbeite in dieser Reihenfolge und stoppe an den markierten Freigabe-Punkten:
1. **`handoff.md` lesen.** Bestätige mir in 58 Stichpunkten, dass du Produkt, Stack und die
STRICT RULES (R1R10, §4) verstanden hast. Wenn die Mockup-Referenz `../bastion` existiert,
sieh sie dir an (Pixel-Vorlage + Tokens).
2. **Offene Punkte (§12) abfragen, BEVOR du baust:** finaler Name (Clusev?), DB (MariaDB?),
GHCR-Namespace, ob `../bastion` vorhanden ist. Bei Unklarheit fragen — nicht raten.
3. **`rules.md` erzeugen** — die STRICT RULES R1R10 aus §4, jede mit kurzer Begründung +
✅korrekt / ❌verboten Code-Beispiel (v.a. R1R5: full-page Livewire-Routen, class statt Volt,
Tokens in `@theme`, keine Inline-Styles außer Progressbar-width, Löschen nur per Modal).
4. **`CLAUDE.md` erzeugen** — Produkt (§1), Stack (§3), Ordnermap (§5), Commands (§9),
Konventionen (UI Deutsch/kein Emoji, Tokens, responsive Mobile+Tablet+Desktop) + eine
"Before-you-code"-Checkliste, die auf `rules.md` verweist.
**FREIGABE-PUNKT:** Zeig mir `rules.md` + `CLAUDE.md`. Erst nach meinem OK weiter.
5. **Scaffold (Docker-first, §8/§9).** Host hat nur Docker → alles im Container bootstrappen:
- `Dockerfile` (php8.3-fpm + composer + node), `docker-compose.yml` (dev: app, vite, reverb,
queue, mariadb, redis — `build:`, bind-mount, Vite remote-HMR `host=0.0.0.0`,
`hmr.host=10.10.90.136`), `docker-compose.prod.yml`, `.dockerignore`.
- Laravel 12 + Livewire 3 (**class, kein Volt**) + Tailwind 4 + Vite.
- `composer require livewire/livewire wire-elements/modal phpseclib/phpseclib laravel/reverb`.
- `resources/css/app.css` mit `@theme`-Tokens aus §6 (aus `bastion.css`).
- Saubere Ordnerstruktur exakt nach §5.
**FREIGABE-PUNKT:** App startet im Container, leere Seite rendert, Vite-HMR läuft. Zeig mir das.
6. **v1 starten (§1 Scope).** Layout + Blade-Components (Sidebar, Topbar, Panel, Kpi, StatusPill,
StatusDot, Badge, Icon[Lucide], Ring), Dashboard als full-page Livewire-Route mit Live-Metriken
über Reverb. Mock-Daten zuerst, SSH-Layer (phpseclib) danach. Alles responsive.
Halte dich strikt an `rules.md`. Konflikt zwischen meiner Anweisung und den Regeln → nachfragen.
Verifiziere jeden Schritt (Container baut, Seite rendert, keine Console-Errors) bevor du
"fertig" sagst. Arbeite auf einem Branch, nicht auf main; committe in sinnvollen Schritten.

306
rules.md Normal file
View File

@ -0,0 +1,306 @@
# 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.