248 lines
13 KiB
Markdown
248 lines
13 KiB
Markdown
# 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`.** 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** (build, render, no console errors, 3 breakpoints), then `git status` for stray
|
||
secrets, commit on the feature branch.
|