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
commit
3e5a1ff81c
|
|
@ -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
|
||||||
|
|
@ -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`.** R1–R10 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.
|
||||||
|
|
@ -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 R1–R5). 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).
|
||||||
|
|
@ -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 5–8 Stichpunkten, dass du Produkt, Stack und die
|
||||||
|
STRICT RULES (R1–R10, §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 R1–R10 aus §4, jede mit kurzer Begründung +
|
||||||
|
✅korrekt / ❌verboten Code-Beispiel (v.a. R1–R5: 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.
|
||||||
|
|
@ -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.
|
||||||
Loading…
Reference in New Issue