# Clusev — Session Handoff (2026-06-14)

Self-contained ramp-up for the next session. Read `rules.md` (R1–R17) + `CLAUDE.md` first;
this file = current state, open items, and the exact work recipes. `handoff.md` is the
original foundational decisions doc (do not overwrite it).

---

## 1. Current state

- **Branch:** `feat/v1-foundation` (never commit to `main`). Remote: Gitea
  `git.bave.dev/boban/clusev.git`.
- **Shipped tags** (all pushed):
  - `v0.3.0` — full bilingual UI (DE default + EN), user-selectable; rule **R16**.
  - `v0.4.0` — change the panel domain from the dashboard (DB snapshot → applied on restart;
    Caddy on-demand TLS; request-derived Reverb endpoint; anti-lockout middleware).
  - `v0.4.1` — fixed a garbled `<header>` on every signed-in page (Blade raw-block bug → rule
    **R17**); made broadcast channels **private** (metrics no longer public).
  - `v0.4.2` — Server-Details UX + hardening polish (see §2b): uniform `danger-soft` buttons,
    installed-only firewall/fail2ban grid, auto-updates as a neutral preference, SSH-verified
    server creation + `pending` status, SSH key-only hint, and the fail2ban/UFW detection fix.
  - `v0.4.3` — responsive audit (R7, all views @ 375/768): hardened mobile overflow from long
    panel domains (`break-all`/`min-w-0`) and gebannte IPv6 in der fail2ban-Liste. Live pages were
    already overflow-free; only data-dependent edge cases fixed.
  - `v0.4.4` — stop iOS zoom-on-focus: one unlayered `@media (pointer: coarse)` rule in `app.css`
    forces 16px on `input`/`textarea`/`select` on touch devices (phone + tablet any size), desktop
    sizing unchanged.
  - `v0.6.0` — **WebAuthn / YubiKey** as an optional login 2nd factor (`web-auth/webauthn-lib`),
    gated on domain+HTTPS (rpId = active domain; hidden on bare-IP). TOTP + backup codes stay the
    required 2FA. Register/list/remove keys in Settings → Security; use a key at the login
    challenge. **DEFERRED MANUAL CHECK:** real YubiKey register + login is browser-verifiable only
    on a domain+HTTPS host (the IP rpId is invalid here) — the ceremony code is unit-tested at the
    option/storage/gating level + Codex-clean, but the end-to-end crypto is unverified until tested
    on a domain.
  - `v0.5.0` — **account recovery** (spec/plan in `docs/superpowers/`): 2FA backup codes
    (encrypted, shown once + downloadable + regenerable, usable at the login challenge),
    forgot-password via 2FA/backup code (email-link path gated on a configured mailer), and a
    `clusev:reset-admin` CLI lockout fallback. Resets rotate `remember_token`; codes consumed
    atomically. **Phase 2 still open: WebAuthn/YubiKey** as a login factor (needs `laragear/webauthn`,
    JS ceremonies, HTTPS/domain) — own spec.
  - `v0.4.6` — bilingual input validation: added `lang/{de,en}/validation.php` (full Laravel 13
    key set + localized `attributes`); previously all field-validation errors were English-only.
  - `v0.4.5` — branding: full favicon/PWA set (`public/favicon.*`, `icon-192/512`,
    `site.webmanifest`) from the orange server brand-mark, wired via `partials/head-icons`; custom
    bilingual error pages (`resources/views/errors/*` + `lang/errors.php`) replacing Laravel's
    defaults, prod `APP_DEBUG=false` pinned, no raw error text. NOTE: unmatched-404 + 503 render in
    the DE default (no session pre-StartSession) — deliberate per R16, custom message still shows.
- **Dev stack** runs on this VM via `docker compose` (app container IP **172.18.0.6**, host
  `10.10.90.136:80`). Managed fleet host = **10.10.90.162** (Debian / apt / ufw; the only
  live-verified target — dnf/zypper/firewalld command strings are built but not live-tested).

---

## 2. RESOLVED — fail2ban & UFW falsely shown "nicht installiert" (fixed `221e49f`)

**Was:** the Hardening checklist showed `fail2ban` and `Firewall (UFW)` as **OFFEN ·
nicht installiert**, while the fail2ban-Status panel below showed `aktiv · 14 gesperrt` with
real SSH-scanner bans. Contradiction.

**Root cause — two different detection methods (same host):**
- `HardeningService::state` (checklist): installed = `dpkg -l <pkg> | grep -q '^ii'` /
  `rpm -q`; active = `systemctl is-active`. → asked "is the **apt/rpm package** recorded?"
- `Fail2banService::status` + `FirewallTool` (panels): installed = `command -v <client>`;
  active = `systemctl is-active`. → asked "is the **binary present + service running**?"
- The host is Debian/apt and fail2ban/ufw is genuinely running, but dpkg does not list it as
  `^ii` (source install, or dpkg `rc` config-only state) → checklist false-negative.

**Fix (committed on `feat/v1-foundation`, NOT yet tagged/pushed):** the checklist's installed
probe in `app/Services/HardeningService.php::state` now ORs three conditions —
package-manager test (`dpkg ^ii` / `rpm -q`) **OR** client binary on PATH (`command -v`)
**OR** service active (`systemctl is-active`) — via the local `$anyOf()` helper, for **both**
fail2ban and the firewall (ufw/firewalld). Consistent with `Fail2banService::status`. Since a
component can never be active without being installed, this only removes FALSE negatives,
never adds a false-positive. `PackageManager`/`FirewallTool` were left unchanged (the OR is
composed in `HardeningService`, not in their primitives).

Verified: new unit test `tests/Feature/HardeningServiceTest.php` (Debian/apt source-install
fallback) **passes**; Pint clean; Codex review clean (no actionable regression).

**Still pending (deferred this session):** (a) live acceptance check against fleet host
10.10.90.162 — load Server-Details and confirm fail2ban/ufw now read "installiert"; (b) no
version bump / CHANGELOG / tag / push — the fix lives on the branch only. The prior session's
"Fix fail2ban/UFW false negative" task chip is now **stale** (work is done).

---

## 2b. DONE — Server-Details UX + hardening polish (2026-06-14)

Seven reported issues fixed on `feat/v1-foundation`, **released as `v0.4.2`** (tagged + pushed). Spec:
`docs/superpowers/specs/2026-06-14-server-details-hardening-polish-design.md`; plan:
`docs/superpowers/plans/2026-06-14-server-details-hardening-polish.md`.

1. **Buttons unified** — `x-btn`: retired borderless `ghost`/`ghost-danger`, added `danger-soft`
   (red-tinted border+bg, always). Löschen/trash now consistent, no hover-only red. All 10 usages
   migrated (servers/settings/files).
2. **Firewall + fail2ban panels** — render only when the tool is installed **or** the read failed
   (`readError`); 2-col `lg` grid when both, full width when one, hidden when absent. Removed the
   redundant "nicht installiert" boxes.
3. **Auto-updates** — reframed as an operator preference: `secure` always true, muted
   aktiv/inaktiv chip (never SICHER/OFFEN). `HardeningService` `neutral` row flag.
4. **Create-server** — verifies the SSH login inside the create transaction via
   `FleetService::testConnection` (echoes a marker, requires it in output — proves exec, tolerates
   missing exit-status). Failure rolls back + shows the SSH reason. New server starts `pending`.
5. **`pending` status** = "Initialisierung" (cyan, never red) across status-pill/dot, server-item,
   fleet index, and the details hero. Promotes on first contact.
6. **SSH key-only hint** on the SSH-Passwort-Login checklist row while password auth is on.

Verified: 16 tests pass (`ButtonComponentTest`, `StatusComponentTest`, `HardeningServiceTest`,
`FleetTestConnectionTest`, `CreateServerTest`, `ServerShowPanelsTest`, `ServerShowSshHintTest`);
Pint clean; R12 browser DE+EN at 375/768/1280 (200, 0 console, 0 leaked tokens); Codex clean after
3 review rounds. **Pre-existing** stock `tests/Feature/ExampleTest.php` still fails (asserts `/` =
200 but the app redirects to login) — unrelated; a task chip was spawned to delete/fix it.

## 3. Other open items / follow-ups

- `resources/js/app.js` `dualChart` is **dead code** — the dashboard chart is a server-rendered
  SVG, nothing subscribes via Echo. Consider removing it (its `Echo.private('metrics')` is the
  only metrics subscriber and it never runs).
- **Caddy on-demand TLS / real ACME issuance is config-validated only** (no Caddy in dev, no
  public DNS). App-side logic (`/_caddy/ask`, snapshot, redirects, Reverb tunnel) is
  browser-verified; the actual cert issuance needs a real prod + DNS test.
- "Install-with-domain → clear-to-bare-IP" is the rough edge: re-run `install.sh` for a clean
  bare-IP realtime (REVERB_* in `.env` otherwise stays domain-shaped).
- firewalld is read-only (rule add/delete is ufw-only) — needs a RHEL/Fedora/openSUSE host to
  finish.

---

## 4. Plugins installed this session (need a SESSION RESTART to load)

Installed via the server's `claude` CLI (`~/.claude/remote/ccd-cli/<ver>`), scope **user**,
into the **local** plugin system (separate from the cloud-synced set):
- `caveman@caveman` — ultra-compressed output (token saver); SessionStart hook auto-activates.
- `superpowers@superpowers-dev` (v5.1.0) — workflow skills (TDD, systematic-debugging, …).
- `ui-ux-pro-max@ui-ux-pro-max-skill` (v2.5.0).

Marketplaces persisted in `~/.claude/plugins/known_marketplaces.json`. They are **not** in the
current session's skill registry until the session is **restarted** (that's why `/caveman:caveman`
still fails this session). After restart: `/caveman:*`, `/superpowers:*`, `ui-ux-pro-max` work.

---

## 5. Work recipes (how to actually verify & ship)

- **Everything in containers (R8).** Pint:
  `docker compose exec -u "${HOST_UID:-1002}:${HOST_GID:-1002}" -T app ./vendor/bin/pint --dirty`.
  Build: same prefix + `npm run build`.
- **R12 — verify in a real browser, BOTH locales:** every touched route → HTTP 200 + 0 console
  errors AND **inspect the rendered DOM** for leaked `@` / `{{ }}` / `$var` / `group.key` text
  (R17 — this is how the v0.3.0/v0.4.0 `<header>` bug slipped: 200 + clean console but garbage
  markup). Recipe: headless Puppeteer image `ghcr.io/puppeteer/puppeteer:latest` on docker
  network `clusev_clusev`, target `http://172.18.0.6`, `NODE_PATH=/home/pptruser/node_modules`.
  Auth via a **TEMP** route `Route::get('/__peek', fn (Request $r) => [AuthFacade::loginUsingId(1),
  redirect($r->query('to','/'))][1])` — **remove before commit** (route:clear after). Locale is
  per-user (`user->locale` wins over session), so to test EN the peek must also persist
  `user->locale`. Dashboard route is `/` (not `/dashboard`).
- **R15 — Codex:** `export PATH="$HOME/.npm-global/bin:$PATH"; codex review --uncommitted` until
  clean (filter node_modules noise).
- **Localization (R16):** every visible string in `lang/{de,en}/<group>.php`, keys identical in
  both. Verify: scan all `__('group.key')` usages resolve via `Lang::has($k,'de'/'en')` and that
  each `lang/de/*.php` group has the same keys as `lang/en/*.php`.
- **Push:** token in `/home/nexxo/.env.gitea` (`GIT_ACCESS_TOKEN`) — read ONLY at push time,
  never echo/commit; sanitize push output
  (`sed -E 's#https://[^@]*@#https://<redacted>@#g; s/[0-9a-f]{40}/<redacted>/g'`).
- **Fleet host safety:** SSH password is in the encrypted vault (never in code/logs). Never
  ban/lock-out the control plane (10.10.90.x).

**Done = ** Pint clean → npm build → R12 (200 + 0 console + DOM scan, DE+EN) → Codex clean →
version bump + CHANGELOG → commit on `feat/v1-foundation` → tag → push (sanitized) → temp
`/__peek` removed.
