14 KiB
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 inapp/Services/(FleetService,MetricsPoller,Provisioner) orchestrate them; queued long ops go inapp/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 intoapp.css. Selection (::selection) and active nav/server tints usebg-accent/25andbg-accent/10+border-accent/25(keep subtle).../bastionmockup 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
@themetoken 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↔ viewlivewire.servers.show(kebab, mirrored path). Components inapp/Livewire/…, views inresources/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 —
/settingsnot/einstellungen,/filesnot/dateien. German lives in the visible nav label, never in thehref/route name. (R13) - Fonts self-hosted only —
.woff2inresources/fonts/(Vite-bundled, relativeurl('../fonts/…')) +@font-faceinapp.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 asHOST_UID(1002 on this VM); run write-tooling asdocker compose run --rm --no-deps -u "${HOST_UID}:${HOST_GID}" app ….
# ── 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. Theappcontainer 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_GIDin.env) — nothing hardcoded. App on :80, Vite remote HMR (server.host=0.0.0.0,hmr.host=10.10.90.136), MariaDB only on127.0.0.1:3306.docker-compose.prod.yml(PROD):image: ghcr.io/OWNER/clusev:<tag>(GHCR namespace is a placeholderOWNER— set via.envbefore 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.shpulls onto the VM. Bootstrap before CI: build on the VM. Usedocker 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$HOMEholds only.env.gitea+ home dotfiles. Host user nexxo = uid 1002 (wp-test=1000,testwp=1001 pre-existed) →HOST_UID/HOST_GID=1002in.env. - Remote: Gitea
git.bave.dev/boban/clusev.git. Credentials live in.env.gitea(untracked) — theGIT_ACCESS_TOKENmust 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). Seerules.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:initpages check the loaded state, not the skeleton. A greenLivewire::testis not enough. Test the 3 breakpoints.
10. Before you code — checklist
- Read
rules.md. R1–R11 are non-negotiable; on any conflict, stop and ask. - New page? → full-page class-based Livewire component + route in
web.php(R1/R2). Never Volt. - Need a color/spacing? → an
@themetoken utility (R3). No raw hex, no inline style except progresswidth(R4). - Destructive/confirm? → wire-elements/modal (R5).
- Place files per the §4 folder map (R6). Class ↔ mirrored kebab view.
- Build responsive: 375 / 768 / 1280; ≥44px touch targets (R7).
- Run all tooling inside the container (R8).
- UI copy German, no emoji; status via color/dots/pills (R9).
- Reuse the token set + Blade component kit (R10).
- 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::testis not enough; build + 3 breakpoints; thengit statusfor stray secrets, commit on the feature branch.