clusev/CLAUDE.md

14 KiB
Raw Permalink Blame History

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, insetbg-surface, bg-raised, …
  • Brand: accent, accent-bright, accent-deep, accent-texttext-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-4text-ink, text-ink-2, …
  • Borders (hairline): line, line-soft, line-strongborder-line, …
  • Fonts: --font-display (Chakra Petch), --font-sans (Space Grotesk), --font-mono (JetBrains Mono) → font-display, font-mono.
  • Radii: --radius-xs/sm/md/lgrounded-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 <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 ….

# ── 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 GHCRdeploy.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. 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 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.
  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.