Commit Graph

87 Commits (54ebb49941b1cc8460122901cf8bf8f6b6daf9fa)

Author SHA1 Message Date
boban 54ebb49941 feat(webauthn): WebauthnService::available gate (domain + https)
WebAuthn is offered only with an active domain (the rpId) over HTTPS; bare-IP/HTTP
returns false so a ceremony never runs with an invalid IP rpId.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 18:18:43 +02:00
boban 78b1017844 feat(webauthn): install web-auth/webauthn-lib + credential storage
Add web-auth/webauthn-lib ^5.3, the webauthn_credentials table (per-user unique
credential_id), the WebauthnCredential model and User hasMany relation.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 18:18:05 +02:00
boban eef79d1758 docs(plan): WebAuthn/YubiKey 2nd-factor implementation plan
TDD plan: install web-auth/webauthn-lib + credential storage, available() gate,
ceremony options/verify (against the installed v5 API), login-challenge use,
settings register/list/remove + JS, and a verification gate. Real key E2E
deferred to a domain+HTTPS host.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 18:16:10 +02:00
boban 27ebc570f4 docs(spec): WebAuthn/YubiKey 2nd-factor design (Phase 2)
Optional security-key 2nd factor via web-auth/webauthn-lib, gated on domain +
HTTPS (rpId = active domain). TOTP + backup codes stay the required 2FA and the
only recovery. Build now with unit/feature tests (mocked ceremony) + R12 gating;
real YubiKey end-to-end deferred to a domain+HTTPS host.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 18:12:32 +02:00
boban 7faa5c7d00 chore(release): v0.5.0
Account recovery: 2FA backup codes (show-once/download/regenerate, usable at the
login challenge), forgot-password via 2FA or backup code (email link gated on a
configured mailer), and a clusev:reset-admin CLI lockout fallback. Resets rotate
remember_token; codes consumed atomically. WebAuthn/YubiKey remains a Phase-2
spec. See CHANGELOG.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 17:14:47 +02:00
boban 197e6f7e49 fix(auth): show the reset-success status on the login page
Both reset paths flash a `status` and redirect to login, but it was never
rendered. Show it as a success banner so the user gets confirmation their
password changed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 17:10:24 +02:00
boban 2ff2cc34c0 fix(auth): toaster on auth pages + audit email-link resets
Codex review follow-ups: extract the notify toaster into a shared partial and
include it in the auth layout too, so sendResetLink + recovery-code regenerate
actually surface their confirmation (the auth layout had no toaster). Record an
AuditEvent for password resets via the email-token path, matching the 2FA path.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 17:07:52 +02:00
boban 3a67aa99ec fix(auth): rotate remember_token on reset + atomic recovery-code use
Address Codex security review:
- ForgotPassword + clusev:reset-admin now rotate remember_token, so a stolen
  remember-me cookie cannot survive a password reset (the email-token path
  already did this).
- useRecoveryCode reads/checks/removes under a row lock (DB transaction +
  lockForUpdate), so two concurrent requests can't both spend the same one-time
  code (replay).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 17:04:12 +02:00
boban f6a65827fd feat(auth): clusev:reset-admin CLI lockout fallback
Shell-only emergency recovery: resets the admin password (prints a random one if
omitted) and, with --disable-2fa, clears the 2FA secret + backup codes so the
operator can re-enroll. Requires server access — the safe last resort.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 16:57:44 +02:00
boban abaed12b0b feat(auth): email reset-link path (gated on a configured mailer)
ForgotPassword gains sendResetLink() + a ResetPassword component at
/reset-password/{token} using Laravel's password broker. The email option is
shown only when mail.default is a real mailer (hidden while MAIL_MAILER=log), so
it activates automatically once SMTP is configured. Tested with Notification::fake.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 16:57:05 +02:00
boban cb8b906aa8 feat(auth): forgot-password reset via 2FA or backup code
"Passwort vergessen?" on login -> a reset view that proves identity with a TOTP
or one-time backup code (no email needed), then sets a new password under the
min(12) mixedCase numbers policy. Rate-limited, generic messages (no account
enumeration), audited.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 16:55:01 +02:00
boban 764e1e5e53 feat(auth): show + download + regenerate 2FA backup codes
2FA confirm now generates 8 backup codes and redirects to a show-once recovery
view (download as .txt, regenerate). Reachable again from Settings. Codes are
shown before the onboarding gate so a fresh enrollee can save them.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 16:52:23 +02:00
boban b2cb94bb32 feat(auth): accept a 2FA backup code at the login challenge
The challenge now falls back to consuming a one-time recovery code when the TOTP
check fails. Relaxed the code input (was numeric, maxlength 6) so a backup code
is typable, and added a hint. Bilingual.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 16:50:36 +02:00
boban f3e595ed45 feat(auth): store 2FA recovery codes (encrypted) on users
Add the two_factor_recovery_codes column (encrypted:array) + User helpers
replaceRecoveryCodes / recoveryCodes / hasRecoveryCodes / useRecoveryCode
(one-time consumption). Hidden from serialization.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 16:48:21 +02:00
boban 63fb1686a4 docs(plan): account recovery implementation plan
TDD plan: recovery-code storage, challenge backup-code acceptance, recovery-codes
view (show/download/regenerate), forgot-password (2FA-code), email reset link
(gated), clusev:reset-admin CLI, and a verification gate.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 16:47:12 +02:00
boban f0a337c8bd docs(spec): account recovery design (forgot-password, 2FA backup codes, CLI)
Phase 1: 2FA recovery codes (encrypted, show-once + downloadable, usable at the
challenge), a forgot-password flow (2FA-code proof primary, email link only when
SMTP is configured), and a clusev:reset-admin CLI lockout fallback. WebAuthn is
deferred to a Phase-2 spec.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 16:41:45 +02:00
boban a0ef0e6d9b chore(release): v0.4.6
Bilingual input-field validation messages: added lang/{de,en}/validation.php
(full Laravel 13 key set + localized attribute names), fixing the English-only
fallback. See CHANGELOG.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 15:47:03 +02:00
boban ecf5bf5fc5 feat(i18n): custom bilingual validation messages (DE + EN)
There was no lang/{de,en}/validation.php, so Laravel fell back to its built-in
English messages for every input-field validation error regardless of locale.
Add the full validation language file in German (source of truth) and English,
plus a localized `attributes` map so :attribute reads naturally (e.g. "SSH-Port",
"E-Mail-Adresse"). Per-form validationAttributes() still override these.

Verified: Validator resolves DE/EN messages + attribute labels; submitting the
empty login form renders "E-Mail-Adresse muss ausgefüllt werden." in German.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 15:45:02 +02:00
boban 75dcfc11d3 chore(release): v0.4.5
Branding + custom error pages: full favicon/PWA icon set wired into both
layouts, and branded bilingual error pages (403/404/419/429/500/503) replacing
Laravel's defaults with APP_DEBUG pinned off in prod — no raw status text or
stack traces. See CHANGELOG.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 15:37:25 +02:00
boban 0fcfd31305 fix(errors): never let locale resolution break the error page
The withExceptions render hook called SetLocale::apply(), which resolves the
user via the DB — if the original error was a DB/user-provider failure, that
second access would throw inside the renderer and prevent the custom error page
from rendering (exactly when it's needed). Wrap it so any failure is swallowed
and the page renders in the default locale.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 15:34:44 +02:00
boban d132efd861 fix(i18n): localize error pages thrown before SetLocale middleware
CSRF (419), unmatched-route 404 and maintenance (503) errors are thrown before
the web-group SetLocale middleware runs, so the custom error pages rendered in
the default locale for EN users. Extract SetLocale::apply() (session-guarded, so
it is safe pre-session) and call it from a withExceptions render hook that falls
through to the normal renderer — the error views now honour the user's language
where it is known.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 15:32:20 +02:00
boban c5d3de6e0b feat(branding): favicons + PWA manifest, and custom branded error pages
Branding assets (public/, served directly):
- favicon.svg (scalable, primary), favicon.ico (16/32/48 PNG-embedded),
  apple-touch-icon.png (180), icon-192/512.png, site.webmanifest. The mark is
  the in-app brand glyph (orange Lucide "server" on dark). Wired into both
  layouts via a shared partials/head-icons (rel=icon svg+ico, apple-touch,
  manifest, theme-color #06080A). nginx serves the manifest as
  application/manifest+json.

Never show raw error text (custom messages only):
- Custom branded resources/views/errors/{403,404,419,429,500,503}.blade.php on a
  self-contained error layout (design tokens, brand mark, bilingual via
  lang/errors.php) — replaces Laravel's default Symfony/whoops pages.
- docker-compose.prod.yml pins APP_DEBUG=false so a prod error can never render a
  stack trace (the custom pages show instead).
- Files download error dispatched a bare $e->getMessage(); wrap it in the
  localized files.download_failed (consistent with upload/delete).

Verified in-browser: icon links present + all assets 200; custom 404 renders the
branded German message (no raw "Not Found"/trace), responsive at 375, 0 console.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 15:27:49 +02:00
boban 07f9110f90 chore(release): v0.4.4
Stop iOS zoom-on-focus: an unlayered @media (pointer: coarse) rule forces 16px
on input/textarea/select for touch devices (phone + tablet of any size), keeping
the dense desktop sizing for mouse pointers. See CHANGELOG.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 14:25:47 +02:00
boban dfe141b406 fix(ui): stop iOS zoom-on-focus by forcing 16px inputs on touch devices
iOS Safari zooms the viewport when a focused form control's font-size is < 16px.
The dense design uses text-xs/sm/[11px] on inputs, which triggered the zoom on
every input across the app. Add one unlayered media query forcing 16px on
input/textarea/select for touch-primary devices (@media (pointer: coarse)) —
covering phones AND tablets of any size (incl. 1024/1366px iPad Pro that a
width cap would miss) while keeping the small desktop sizing for mouse pointers.
Unlayered so it wins over Tailwind's utility classes.

Verified via touch emulation: inputs render 16px on phone/iPad/iPad-Pro widths
(matchMedia(pointer:coarse)=true) and stay 14px on mouse desktops at any width.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 14:24:05 +02:00
boban cc00aa7d87 chore(release): v0.4.3
Responsive audit (R7) over all views at 375/768: hardened mobile overflow from
long panel domains (break-all/min-w-0) and banned IPv6 addresses in the fail2ban
list. Live pages were already overflow-free; only data-dependent edge cases
fixed. See CHANGELOG.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 13:07:09 +02:00
boban 36f5f5c514 fix(ui): prevent mobile overflow from long URLs and IPv6 bans
Responsive audit (R7) at 375/768 — all pages were overflow-free with current
data, but three elements overflow with long data the live fleet doesn't have:
- system: the panel URL link (domain up to 253 chars) had no break-all, and the
  access-address dd had truncate without min-w-0 (so a flex child won't clip).
  Add break-all to the link, min-w-0 to the dd, shrink-0 to its label.
- servers/show: the fail2ban banned-IP span had no min-w-0/break, so a banned
  IPv6 pushed the row past 375px. Add min-w-0 break-all + shrink-0 on the button.

Verified by injecting a 100-char domain and long IPv6 into those elements at
375px: document no longer overflows (was 1685px → 375px).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 12:38:47 +02:00
boban e324b14014 docs: record v0.4.2 release in handoff
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 11:03:32 +02:00
boban e706b1e922 chore(release): v0.4.2
Server-Details UX + hardening polish: uniform bordered buttons (danger-soft),
installed-only firewall/fail2ban grid, auto-updates as a neutral preference,
SSH-verified server creation with a "pending" (Initialisierung) status, and an
SSH key-only checklist hint — plus the fail2ban/UFW "nicht installiert"
false-negative fix. See CHANGELOG.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 11:01:41 +02:00
boban 2b3bab7848 docs: record server-details UX + hardening polish as done
Note the seven fixes (buttons, panel grid, auto-updates reframe, create-verify,
pending status, SSH hint) as completed on the branch, with the verification
trail, and flag the pre-existing stock ExampleTest failure.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 10:49:52 +02:00
boban 37efa64b9d fix(ssh): require an exec marker in the connection probe
Auth success alone does not prove the server can run commands (a server that
authenticates but rejects exec channels would be accepted, yet every fleet
operation runs commands). The probe now echoes a unique marker and requires it
in the output — proving exec works, while still tolerating servers that omit
the optional exit-status packet.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 10:46:59 +02:00
boban 07f0a0cb00 fix(ssh): accept SSH servers that omit an exit-status packet
testConnection gated success on `exit code === 0`, but phpseclib returns false
from getExitStatus() when a server omits the optional exit-status message, which
would reject a valid login with an empty error. A successful connect() already
proves the credential (it throws on auth failure), so the probe now succeeds on
any exception-free connect + exec.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 10:43:57 +02:00
boban bc0691a730 fix(servers): keep read-error panels visible + pending header support
Address Codex review of the panel-grid + pending-status work:
- Firewall/fail2ban services report installed=false together with readError=true,
  so gating panel visibility on `installed` alone hid the read-error message and
  made a failed probe indistinguishable from an absent tool. Show the panel when
  installed OR readError.
- The server-details hero only mapped online/warning/offline, so a freshly
  created `pending` server rendered an untranslated "Pending" label and an
  uncolored icon. Add the localized cyan pending state to $statusLabel + hero.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 10:41:04 +02:00
boban a5c518822b feat(servers): hint that disabling password login leaves key-only access
Adds a muted one-line hint on the SSH-Passwort-Login checklist row while
password auth is still on, so the operator knows access becomes SSH-key only
(and to deposit a key first) before toggling — mirroring the lockout guard.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 10:30:07 +02:00
boban 21ee81f742 feat(servers): show firewall/fail2ban only when installed, in a grid
Each panel renders only when its tool is actually installed (the Sicherheit
checklist carries the install/Aktivieren path otherwise), removing the redundant
"nicht installiert" boxes. Both shown -> 2-col responsive grid (lg, items-start);
one shown -> full width. Trash uses danger-soft, unban uses secondary.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 10:28:55 +02:00
boban a9334d11de feat(servers): verify SSH on create + start in "Initialisierung"
CreateServer now probes the entered SSH login inside the create transaction; a
failed probe throws and rolls back (no half-registered server), surfacing the
SSH reason on the form. Successful creation starts the server as `pending`
("Initialisierung") instead of red/offline until first contact promotes it.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 10:26:15 +02:00
boban 2c25303903 feat(ssh): add FleetService::testConnection credential probe
Connect + trivial exec against a server's stored credential; never throws,
returns {ok,error}. Used by the create-server guard to verify the SSH login
before persisting.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 10:24:42 +02:00
boban ed909edf3e feat(hardening): treat auto-updates as a neutral operator preference
Automatic updates are a choice, not a security gate: the unattended row now
reports secure=true always and carries a `neutral` flag. The checklist renders
it muted (aktiv/inaktiv) instead of the green/orange SICHER/OFFEN verdict, so
"off" is never flagged as insecure. Detection is unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 10:23:28 +02:00
boban 615f440515 feat(ui): add "Initialisierung" (pending) server status
New `pending` status renders cyan (dot + pill, soft ping) instead of red, for
freshly created servers that have not been contacted yet. Wired into the
status-pill/dot components, the fleet list, and server-item, with a bilingual
`servers.status_pending` label.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 10:20:32 +02:00
boban e26921f33a feat(ui): uniform bordered button kit; retire ghost variants
Every x-btn variant now carries a persistent border + background. Replace the
borderless `ghost`/`ghost-danger` (the latter only reddened on hover) with the
bordered `secondary` and a new `danger-soft` (red-tinted border+bg, always).
Migrate all 10 usages across servers/settings/files.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 10:18:47 +02:00
boban 50b18aa45f docs(plan): server-details UX + hardening polish implementation plan
Eight-task TDD plan covering the seven fixes: button kit, pending status,
auto-updates reframe, FleetService::testConnection, create-verify, panel grid,
SSH hint, and a final R12+Codex gate.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 10:17:28 +02:00
boban a0bc8d41cb docs(spec): server-details UX + hardening polish design
Design for seven reported fixes: unified bordered button kit (retire
ghost/ghost-danger, add danger-soft), firewall+fail2ban panels in an
installed-only responsive grid, auto-updates reframed as a neutral operator
preference (never an insecure verdict), create-server SSH verification with
atomic rollback, a new "Initialisierung" (pending) server status, and an
SSH key-only hint on the checklist. Includes file map, TDD + R12 plan.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 10:09:17 +02:00
boban 9c3094cc9a docs: mark fail2ban/UFW detection bug resolved (221e49f)
The detection false-negative was already fixed in the working tree; it is now
committed on feat/v1-foundation with a passing unit test, Pint clean, Codex
clean. Update the handoff so the next session sees it as resolved rather than
open. Notes the still-pending items (live acceptance check on 10.10.90.162; no
version bump/tag/push yet) and that the prior task chip is now stale.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 09:23:15 +02:00
boban 221e49f24a fix(hardening): detect fail2ban/ufw as installed via binary or active service
The hardening checklist read "installed" from the package manager alone
(dpkg `^ii` / rpm `-q`), so it disagreed with the live fail2ban/firewall
panels, which use `command -v` + `systemctl is-active`. On a host where the
component is genuinely present but not recorded as an apt `^ii` package —
installed from source, or left in dpkg `rc` (config-only) state — the
checklist showed `nicht installiert` while the panel reported it active with
real bans (the reported contradiction).

The installed probe now ORs three conditions: the package-manager test, the
client binary on PATH, and the service already running. Since a component can
never be active without being installed, this only removes FALSE negatives —
it can never produce a false positive. Detection is now consistent with
Fail2banService::status and FirewallTool. Applies to both fail2ban and the
firewall (ufw/firewalld).

Adds HardeningServiceTest covering the Debian/apt source-install fallback:
the emitted probe gains `command -v fail2ban-client` and `dpkg -l ufw`, and an
active component reads as featureOn.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 09:22:28 +02:00
boban 2930f16d12 docs: session handoff (state, open fail2ban/UFW detection bug, work recipes)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 08:57:04 +02:00
boban f5f903046d fix(blade): garbled header on every signed-in page + private metrics channel (v0.4.1)
Two fixes/hardenings on top of v0.4.0:

1) Blade raw-block bug (R17). The topbar's @php block had a COMMENT containing the literal
   tokens @php/@endphp; Blade's non-greedy raw-block matcher took the @endphp in the comment
   as the block end and dumped the remainder ("$title ??= …") as plain text into <header> on
   every authenticated page (and left the page title empty). R12 missed it: the page still
   returned 200 with no console error and the <title> comes from the Livewire component. Fixed
   the comment; added rule R17 (no directive tokens in Blade comments; block over inline @php;
   R12 must inspect the rendered DOM, not just status/console) to rules.md + CLAUDE.md.

2) Broadcast channels are now PRIVATE (security hardening — closes the follow-up flagged in
   v0.4.0). MetricsTicked rides a PrivateChannel authorized via /broadcasting/auth
   (withBroadcasting + routes/channels.php), so fleet metrics can no longer be subscribed to by
   anyone holding the bundled app key — including over a stale Caddy host after a domain change.
   The channel callback requires User::securityOnboarded() (rotated password + 2FA), mirroring
   the panel's EnsureSecurityOnboarded gate — authentication alone is not enough. Echo sends the
   CSRF token for the auth handshake. Convention documented (CLAUDE.md §3 / channels.php): every
   channel is private.

Bump 0.4.0 -> 0.4.1; CHANGELOG.

Verified: Pint clean; npm build; R12 all routes 200 + 0 console errors (both locales); topbar
<header> rendered text clean (no @/{{ }}/$var/key leaks); private broadcast publishes to
reverb:8080 and /broadcasting/auth + the onboarding-gated callback authorize correctly; Codex
review clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 08:34:07 +02:00
boban a44e29426a feat(domain): change the panel domain from the dashboard (v0.4.0)
The panel domain is editable on the System page again — safely this time. It is
stored in the DB (Setting `panel_domain`, overriding install-time APP_DOMAIN) and
APPLIED ON RESTART, never mid-session: a snapshot file (storage/framework/active-domain)
is frozen at container start by the entrypoint (`clusev:snapshot-domain`), so saving a
new domain only takes effect after `docker compose ... restart`.

How it works
- DeploymentService: pending (configuredDomain, DB) vs active (domain(), snapshot file);
  setDomain() persists; restartPending() drives the UI notice. Snapshot reads the DB
  DIRECTLY (cache-independent) and retries until the settings table is readable; if it
  never is, it freezes the env fallback so the active domain is always a FIXED value
  (never a live one that could shift after startup).
- AppServiceProvider: derives app.url from the active domain at boot; pins server->Reverb
  publishing to the internal reverb:8080 (domain/cert-independent).
- Caddy: on-demand TLS gated by /_caddy/ask (issues a cert only for the configured
  domain); HTTP always served for bare-IP recovery; /app,/apps forced to HTTPS for any
  hostname (plaintext only on a bare IP).
- Reverb client endpoint is derived from the live request and rides the same front door
  (/app tunnel — Caddy in prod, nginx in dev), so realtime follows a domain change with
  no JS rebuild and no stale .env value.
- System page: domain form + R5 confirm + "restart required" notice with the exact
  command; DE/EN strings (R16).

Anti-lockout / security
- session.secure + the HTTPS redirect follow the real request scheme; bare-IP HTTP is
  always a recovery path. trustProxies('*') only in production (dev can't be tricked into
  faking HTTPS via X-Forwarded-*). When a domain is active only that domain (HTTPS) and
  the literal server IP (HTTP) serve the panel; any other/stale host is refused (404),
  and IP-recovery redirects stay on the IP.

Bump 0.3.0 -> 0.4.0; CHANGELOG. Follow-up tracked: make the public `metrics` broadcast
channel private (wire broadcasting auth).

Verified: Pint clean; npm build; Caddyfile validates; R12 all routes 200 + 0 console
errors; Echo connects via the unified /app tunnel; domain set/clear + restart-gating +
/_caddy/ask (200 active / 403 other) + host-enforcement matrix all confirmed in dev;
Codex review iterated to no actionable in-scope findings; 14-agent adversarial
lockout/security review (real trustProxies finding fixed).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 01:38:16 +02:00
boban ec1516184e i18n: complete DE/EN localization (modals, shell, backend) + v0.3.0
- modals: wire 5 hard-coded views + 10 components to lang/{de,en}/modals.php
- shell: command-palette (incl. app.js cmdk(nav,actions) refactor), server-item,
  toaster fallback
- backend: new lang/{de,en}/backend.php; localize Hardening/Firewall/Fail2ban/
  Maintenance services, OsProfile, plus FleetService + CredentialVault errors
- fix: localized page <title> on Dashboard/Servers index+show (->title(), the
  Livewire-3 way; the stray title() methods were never auto-called)
- fix: topbar inline @php() was swallowed by Blade's raw-block precompiler up to a
  later @endphp -> 500 on every authed page; converted to block @php ... @endphp
- versions: localize changelog section labels via a canonical heading map
- translation-quality pass: Unlock, Add credentials, clearer auth/notify copy

Bump 0.2.1 -> 0.3.0; CHANGELOG.

Verified: Pint clean; npm build OK; R12 all routes HTTP 200 + 0 console errors in
BOTH locales (titles/nav/labels switch language); 589 used keys resolve in de+en
with full group parity; Codex review clean; adversarial DE/EN audit (0 high).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-13 23:27:42 +02:00
boban 5691051ad8 feat(i18n): bilingual foundation (DE/EN) + localize auth/dashboard/servers/fleet/account
Adds user-selectable German (default) + English, per new rule R16.

Foundation:
- config locale=de + fallback=de; User.locale column (+migration); SetLocale middleware
  (user → session → default; SUPPORTED=[de,en]); public /locale/{code} route; DE/EN
  switcher (x-lang-switch) in topbar + auth layout; <html lang> dynamic.
- Translation structure: lang/{de,en}/<group>.php (one group per feature + shared common),
  identical keys across both languages, :placeholder interpolation. Documented as R16 in
  rules.md + CLAUDE.md (R9 reconciled).
- Removed the "Warum Caddy" rationale block from the System page (per feedback).

Localized this pass (views + Livewire components, keys de↔en at parity):
- common, auth, dashboard, servers, services, files, audit, settings, system, versions.
- #[Title] attributes → dynamic title() (attributes can't call __()).

Remaining (next commit): modals components + a few modal views, shell (command palette,
nav-item, server-item, toaster), and backend service messages (app/Services/*, OsProfile).
App is fully functional in German throughout; English covers the groups above.

Boot-verified: /login HTTP 200 in DE + EN, titles + <html lang> localized, locale switch works.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-13 22:36:16 +02:00
boban c34ced0c0e fix(tls/ui): fully automatic TLS (status-only) + uniform hardening buttons
Per operator feedback ("das soll automatisch sein, die Caddy-Config muss man nicht
sehen, nur 'SSL steht'"):

- TLS is fully automatic via Caddy (auto-issue + auto-renew + HTTP→HTTPS). The System
  page now shows only the STATUS (domain + access URL) — the generated Caddy config and
  the manual `caddy reload` command are gone, and the domain/email edit form is removed.
- The panel domain is an INSTALL-TIME value (APP_DOMAIN → config('clusev.domain')), so it
  stays consistent with APP_URL, the Reverb (wss) endpoint and cookie security — none of
  which can be re-derived at runtime (Caddy's site address + the browser's Reverb host are
  fixed at install/build, and .env is never rewritten). DeploymentService is now read-only.
- Removed AppServiceProvider's runtime app.url override (it let a stale DB value shadow the
  install-time URL). Migration drops the inert empty dashboard_domain/dashboard_email rows
  (a real configured value is preserved as history; re-apply via install.sh — see CHANGELOG).
- Hardening checklist: all toggle buttons share one style (secondary/bordered) instead of
  varying with state (R10).

Bump 0.2.0 -> 0.2.1 + CHANGELOG. Pint clean; Codex reviewed (the remaining note is a
deliberate install-time-domain tradeoff — a runtime/DB domain cannot be served by the
templated Caddy without a false "TLS active" status); R12 — system/server-show/dashboard
HTTP 200, 0 console errors, raw config + edit form gone, buttons uniform.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-13 21:17:26 +02:00
boban 4a4d107bfc chore(release): v0.2.0 — System TLS rationale + version bump + CHANGELOG
- System page: collapsible "Warum läuft TLS über Caddy?" explaining the single
  host-exposed TLS terminator (auto Let's Encrypt + auto-renew + wss for Reverb,
  decoupled from app updates) — answers the operator's question in the UI; Caddy stays.
- Bump 0.1.1 -> 0.2.0 (cross-OS management, firewall + fail2ban control, command
  palette) + CHANGELOG entry.

Pint clean; Codex review clean; R12 — all 9 routes HTTP 200, 0 console errors,
TLS note renders.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-13 20:43:07 +02:00