# 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 13** | | 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-hosted only** (`public/fonts/*.woff2` + `@font-face` in `app.css`), **never a CDN/Google Fonts link or `@import`** (R14) | > **Version note (2026-06-11):** handoff §3 specifies Laravel 12; **Laravel 13** is used (it is the current release; user decision). Livewire 3 / Tailwind 4 / wire-elements/modal / Reverb are unaffected; PHP pinned to **8.3**. **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) - **URLs / route binding:** records exposed in URLs use a **UUID** route key, never the integer PK (`getRouteKeyName(): 'uuid'`). (R11) - **Route paths + names are English**, always — `/settings` not `/einstellungen`, `/files` not `/dateien`. German lives in the visible nav label, never in the `href`/route name. (R13) - **Fonts self-hosted only** — `.woff2` in `resources/fonts/` (Vite-bundled, relative `url('../fonts/…')`) + `@font-face` in `app.css`; never a Google Fonts / CDN `` or `@import`. (R14) - **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 + **vite** via supervisor — ONE container), > `reverb`, `queue`, `mariadb`, `redis`. App on **:80**, Vite HMR on **:5173** > (`hmr.host=10.10.90.136`). Vite is **not** a separate service. Containers run as `HOST_UID` > (1002 on this VM); run write-tooling as > `docker compose run --rm --no-deps -u "${HOST_UID}:${HOST_GID}" app …`. ```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. The `app` container runs php-fpm + nginx + Vite (HMR) via supervisor (one unit). Ports/UID are **env-driven** (`APP_PORT`, `VITE_PORT`, `REVERB_HOST_PORT`, `DB_PORT`, `HOST_UID`/`HOST_GID` in `.env`) — nothing hardcoded. App on **:80**, Vite remote HMR (`server.host=0.0.0.0`, `hmr.host=10.10.90.136`), MariaDB only on `127.0.0.1:3306`. - **`docker-compose.prod.yml` (PROD):** `image: ghcr.io/OWNER/clusev:` *(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/clusev`** (its own git repo). The parent `$HOME` holds only `.env.gitea` + home dotfiles. Host user **nexxo = uid 1002** (`wp-test`=1000, `testwp`=1001 pre-existed) → `HOST_UID/HOST_GID=1002` in `.env`. - **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:** `.env*` (incl. `.env.gitea`) + keys are gitignored; never commit/echo the Gitea token (read it only at push time). See `rules.md` → Appendix. - **Verify before "done" (R12):** load **every touched page in a real browser** (headless ok) → **HTTP 200** (no 500) + **zero console/network errors**; for lazy `wire:init` pages check the **loaded** state, not the skeleton. A green `Livewire::test` is **not** enough. Test the 3 breakpoints. --- ## 10. Before you code — checklist 1. **Read `rules.md`.** R1–R11 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 in a browser (R12)**: every touched route loads at **HTTP 200** with **zero console errors** (loaded state of lazy pages, not the skeleton) — a green `Livewire::test` is not enough; build + 3 breakpoints; then `git status` for stray secrets, commit on the feature branch.