# 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.** **Convention: every broadcast channel is
  PRIVATE** (authenticated via `/broadcasting/auth`) — the app key is bundled into the JS and the
  Reverb socket can answer on a stale Caddy host, so a public channel would leak fleet data. Declare
  events with `PrivateChannel`/`PresenceChannel` and authorize in `routes/channels.php` (see the
  header comment there); per-server channels also scope to records the user may see.
- **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 is localized (DE default + EN)** — never hard-code visible strings; use
  `__('group.key')` with translations in `lang/{de,en}/<group>.php` (one group per feature +
  shared `common`), keys identical in both languages. Register: terse/operational, **no emoji**
  (status = color/dots/pills). Native technical tokens stay as-is (`nginx.service`, `SSH`, `2FA`). (R9, R16)
- **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)
- **Blade:** use block `@php … @endphp` (not inline `@php(...)`); never put the literal tokens
  `@php`/`@endphp` in a comment/text — the raw-block matcher eats them. (R17)
- **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 `<link>` 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:<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/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;
    **inspect the rendered DOM** (not just status/console) for leaked `@`/`{{ }}`/`$var`/`group.key`
    text — those return 200 too (R17); build + 3 breakpoints; then `git status` for stray secrets, commit.
11. **Codex review (R15)**: run **`/codex:review`** over the change; it must report **no errors and
    no security issues**. Fix + re-run until clean. The task is not done until Codex passes.
