clusev/CLAUDE.md

248 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

# 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-host `.woff2`, no CDN in prod |
> **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)
- **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":** container builds, page renders, no console errors. Test the 3 breakpoints.
---
## 10. Before you code — checklist
1. **Read `rules.md`.** R1R11 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.