Commit Graph

431 Commits (a6176341e290c2c981c0606fb483d355b1c953d0)

Author SHA1 Message Date
boban 8e9499780e fix(release): review polish — promote idempotency, CI hardening, docs
- promote.sh: fail fast before mutating the public tree if the tag was already
  promoted (avoids a half-commit left on the runner on re-dispatch).
- ci-staging.yml: pass STAGING_PROJECT_DIR via env + :? guard instead of a
  ${{ }} expression in run: (clears the template-injection lint, fails loudly
  on misconfig).
- README: PUBLIC_REPO_URL was never read by any workflow — keep only the slug.
- set-repository-url.test.sh: chmod +x for mode parity with promote.test.sh.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 21:18:44 +02:00
boban 4aa55b7124 ci: workflow_dispatch promotion to public (beta) + beta-to-stable
Promotion workflows for the release pipeline. promote-public publishes a tag to
the public repo's beta channel; promote-stable re-tags a finalised beta as a
stable vX.Y.Z. Both bound to the 'production' environment (required reviewer),
reading PUBLIC_REPO_SLUG/PUBLIC_REPO_TOKEN. Tree copy via scripts/promote.sh
excludes .git/.github so private CI never reaches the public repo.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 21:18:32 +02:00
boban e2dc9b17a4 docs: release pipeline (3-repo promotion, channels, wiring)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 21:10:03 +02:00
boban c4f90b0830 ci: test on v* tags, optional self-hosted staging deploy on betas
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 21:03:14 +02:00
boban 79bd08b0d8 fixup(release): promote.sh --allow-empty so identical-tree stable promotion still tags
Stable re-tags the same source commit as the last beta, so the extracted tree
is byte-identical and `git commit` would abort on "nothing to commit" under
set -euo pipefail, leaving the stable tag uncreated. Use --allow-empty and add
a promote.test.sh case that promotes a second identical-tree tag into the same
pub repo to guard the regression.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 20:59:24 +02:00
boban 0dd7564ab0 feat(release): promote.sh — clean per-release tree copy to the public repo
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 20:53:44 +02:00
boban c6e759a3b4 fixup(install): align set-repository-url write-back with plan if/else structure
Restore the plan's if-grep / replace / else-append skeleton in the
CLUSEV_REPOSITORY write-back. The replace branch deletes existing
line(s) then appends once so duplicates collapse to a single entry,
keeping scripts/set-repository-url.test.sh green (the plan's verbatim
in-place sed leaves two identical lines and fails the duplicate-line
assertion).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 20:46:59 +02:00
boban ee111bfa9a feat(install): derive CLUSEV_REPOSITORY from the clone origin
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 20:43:25 +02:00
boban a1386b0533 refactor(config): repository URL is env-driven (no private URL in tracked files)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 20:35:59 +02:00
boban 06b5600641 docs(plan): release-promotion phase A implementation plan
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 20:33:03 +02:00
boban 30034a564b docs(spec): full release model — channels, dashboard buttons, two phases
Capture the maintainer's release workflow: dev-dashboard buttons drive the
pipeline (Deploy to Staging / Deploy to Public / Promote to Stable). Beta =
vX.Y.Z-betaN on staging, Stable = vX.Y.Z on public; public serves both channels.
Public promotion triggered via the GitHub API with a deploy token in the
encrypted vault. Decomposed: Phase A = git/CI/CD + config-URL infra (buildable
now, URL-agnostic); Phase B = the dashboard release-control UI (own spec, after
A + the repos exist).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 19:34:21 +02:00
boban 70da2718fb docs(spec): clarify URL handling — public default in config, private URLs only in .env
Per the maintainer: dev/staging may hold any URL (in their gitignored .env);
the public repo's tracked files carry only the public URL. So config/clusev.php
defaults to the public URL, private (Gitea/GitHub-private) URLs live solely in
gitignored .env, and install.sh derives CLUSEV_REPOSITORY from the clone origin.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 11:22:34 +02:00
boban e2b4a968c9 docs(spec): 3-repo release promotion (Gitea → GitHub private → public)
Design for promoting Clusev dev → staging → public across three repos with
GitHub Actions CI, a manual gate before public, and a clean public history.
Core: no repository URL in tracked files — derived from the clone origin, kept
in gitignored .env per environment, so the public repo never exposes the
private/Gitea URLs and the URLs plug in later when the GitHub repos exist.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 07:03:24 +02:00
boban 4ca2ed96d5 feat(brand): source-view comment + console branding with self-XSS warning (v0.9.56)
- A friendly HTML comment in the page source (both layouts, before <head>) —
  ASCII "CLUSEV", what it is, and the project link — for anyone who View-Sources.
- A styled console banner printed on load (every page, incl. login): the CLUSEV
  wordmark, a one-liner, the repo link, AND a self-XSS security warning. The
  warning earns its place: a Linux-server control panel is a prime target for
  "paste this into your console" social engineering, so the notice sits exactly
  where that attack happens. console styling is best-effort and never throws.

Verified: /login 200, the console logs the brand + warning + link, the source
carries the comment, no page errors; 374 tests pass; Vite build clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 06:19:46 +02:00
boban 8914f5f746 fix(files): responsive file rows + restore button cursor (v0.9.55)
- File manager: on tablet widths the lg table grid squeezed the name column
  (minmax(0,1fr)) to near-zero behind the perms/size/modified columns + wide
  action buttons, so the filename was unreadable/overlapped. The rows now render
  as a stacked card below xl — name on its own full-width line, a folded
  perms·size·modified line and the actions beneath — and only switch to the
  table grid at xl (tightened columns). Mobile tidied up to match.
- Restore the pointer cursor: Tailwind v4's preflight makes buttons use the
  default cursor, so clickable controls (folder names, every button) didn't show
  a hand. Added a base rule giving enabled button/[role=button]/summary/label
  the pointer cursor.

Verified the row at desktop / tablet / mobile widths (name readable at every
breakpoint, table only when wide). 374 tests pass (the Livewire test renders the
blade), Tailwind build clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 06:06:32 +02:00
boban 359aa86aea fix(files): binary file content no longer breaks the Livewire snapshot (v0.9.54)
Opening a binary / non-UTF-8 file in the file manager threw "undefined is not
valid JSON". The editor bound the raw file bytes to its public $content
property; Livewire JSON-encodes the component snapshot, and invalid UTF-8 makes
PHP's json_encode return false, so the client received an empty response and
JSON.parse("undefined") failed (the trace in mergeNewSnapshot/deepClone).

Fixed at both layers: FleetService::readFile blanks the content when it detects
binary/non-UTF-8 (returns the binary flag instead), and FileEditor::load only
binds content for non-binary, non-oversize files. Binary/oversize files show
their existing notice; the snapshot is always valid UTF-8.

Tests: load blanks binary content (keeps the snapshot valid) and keeps valid
text content. 374 pass, Pint clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 05:49:05 +02:00
boban 7a6d20cbed fix(ui): add wire:key to list rows — stops "pending is not defined" (v0.9.53)
Lists rendered loop rows without a wire:key. On re-render Livewire's morph
matched rows by position and cloned them, dropping the inline Alpine x-data
(modalTrigger) scope on the buttons — the console threw "Alpine Expression
Error: pending is not defined" (and a follow-on "undefined is not valid JSON"
as the snapshot merge broke). Reported in the file manager; the same pattern
existed across the app.

Added a stable wire:key to every loop whose rows carry inline Alpine / x-modal-
trigger state: files (entries + breadcrumbs), services, the release-channel
selector, settings (users, webauthn keys, login-protection bans) and the server
detail page (hardening, firewall rules, SSH keys, fail2ban exceptions). Keys use
a stable identifier (id / fingerprint / md5 of the natural key), not the loop
index.

372 tests pass (the Livewire tests render these blades, so a broken key would
fail them); /system, /settings, /files load 200 with no console errors.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 00:44:03 +02:00
boban 91be170e78 fix(dashboard): readable audit labels in the recent-events panel (v0.9.52)
The /audit page already rendered human-readable action labels (v0.9.48), but the
Dashboard's "recent events" panel still showed the raw codes (wg.set-endpoint,
deploy.update_request). It now uses the same AuditEvent::action_label + is_error
styling, so failures show red with an alert icon — consistent with the audit page.

Test: dashboard recent events render the readable label, not the raw code. 372
pass, Pint clean, dashboard loads 200 with no console errors and no raw codes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 00:27:13 +02:00
boban 4b2aee3941 fix(versions): show what's-new (v-prefixed ref) + sidebar update badge (v0.9.51)
Two fixes the operator asked for:

- The "what's new" preview was empty. availableChangelog fetched CHANGELOG.md at
  ref="0.9.50", but release tags are "v0.9.50" — the raw fetch 404'd and the
  panel silently stayed empty. Use the v-prefixed tag as the ref. The existing
  test masked it with a wildcard fake; tightened with Http::assertSent.

- The sidebar now shows a "1" badge on the Version item when an update is
  available, so an update is visible on every page, not only the Versions page.
  Driven by a new ReleaseChecker service: updateAvailable() is a pure cache read
  (no network in the sidebar); a scheduled clusev:check-update (every 30 min,
  anonymous/read-only) keeps the cache warm so the badge is accurate even before
  the operator opens the Versions page.

Refactor: the remote tag lookup (fetch + newest-version) moved out of the
Versions component into ReleaseChecker; the component delegates. nav-item gains
an optional badge prop.

Tests: ReleaseChecker (cache-read update-available, refresh caches, command warms
cache, sidebar badge renders only when available) + the v-prefixed-ref assertion.
371 pass, Pint clean, /versions loads 200 with no console errors; badge + the
"What's new in v0.9.50" panel verified in the browser. Lang parity kept.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 00:19:27 +02:00
boban c6597be59e feat(audit): paginate the log + DB-wide search (v0.9.50)
The audit log loaded the latest 50 entries and filtered them in memory, so it
couldn't grow and search only spanned those 50. Now it is paginated (25/page)
with the page deep-linked in the URL via Livewire WithPagination (?page=), and
the search runs in SQL across the whole history — matching actor / action code /
target / server name AND the readable labels (a term is translated to the action
codes whose localized label contains it, so "Endpoint geändert" still finds
wg.set-endpoint). Pagination control mirrors the changelog's windowed pager.

Tests: 25/page with page 2 overflow, search resets to page 1, search filters in
SQL across all pages (a match on page 2 surfaces when filtered). 365 pass, Pint
clean, /audit loads 200 with no console errors, audit lang parity 62/62.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 23:58:27 +02:00
boban 6d9dcb9529 feat(versions): show the available release's changelog before updating (v0.9.49)
The Versions page only rendered the INSTALLED CHANGELOG.md, so the operator
couldn't see what an available update contained until after applying it. Now,
when behind a published release, a "What's new in vX.Y.Z" panel shows the
changelog of the available version(s) — fetched from the remote CHANGELOG.md at
the latest tag (Gitea raw API, anonymous/read-only, briefly cached), parsed with
the shared changelog parser and filtered to versions newer than installed.

Degrades gracefully: a failed fetch yields an empty preview and never blocks the
update button. Refactored releases() to a reusable parseChangelog().

Tests: preview lists only newer versions + renders their entries; no preview
when current. 362 pass, Pint clean, /versions loads 200 with no console errors,
versions lang parity 51/51.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 23:48:37 +02:00
boban e154f7d92f feat(audit): readable action labels + failures surfaced (v0.9.48)
The audit log showed raw machine codes ("Administrator · wg.set-endpoint").
Now each entry renders a localized human-readable label ("WireGuard endpoint
changed"), via an AuditEvent::action_label accessor backed by an audit.actions
lang map (DE/EN); unmapped/dynamic codes (e.g. harden.*.on|off) fall back to a
tidied form, never a bare code. Search also matches the readable label.

Failures are now visible: AuditEvent::is_error flags failed/security events
(failed sign-in, IP ban, failed 2FA, wg.action-failed) which render in red with
an alert icon. And failed WireGuard dashboard actions are now written to the
audit log with the host error message (not just a transient toast), so the
operator can read later what went wrong.

Tests: label mapping + fallback, is_error, page renders label not code, search
by label, WG failure auditing. 360 pass, Pint clean, /audit loads 200 with no
console errors, audit lang parity 59/59.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 23:41:48 +02:00
boban c89b28e1ef ux(wg): endpoint field is host-only (no apparent double port) (v0.9.47)
The server config showed a "Port" and an "Endpoint" that repeated the same
:51820, so it looked like the port had to be entered twice. It never did (since
0.9.43 a bare host gets the listen port appended) — but the UI didn't say so.
Now the field is labelled "Public endpoint (host)", the placeholder shows only
the host (port stripped via Str::before), and the hint explains the port is
taken from the listen port automatically; a different port is only needed for
port-forwarding (host:port). Display/copy only — no behaviour change.

Pint clean, /wireguard server tab loads 200 with no console errors, lang
parity 89/89, 354 tests pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 23:25:44 +02:00
boban 645d7dd4a4 fix(wg): traffic header shows live peer totals; QR-name limit made explicit (v0.9.46)
- The Traffic panel header ("Empfangen/Gesendet") drew from the sampled
  throughput, which is 0 until the minutely sampler accrues deltas — so a peer
  with live traffic (e.g. 244 B) showed "0 B" in the header. It now sums the
  live per-peer rx/tx counters, so the header always matches the peer rows. The
  chart below stays windowed throughput (fills from the sampler).
- import_hint reworded to state plainly that the WireGuard phone app ALWAYS
  prompts for a tunnel name on a QR scan (app behaviour, not changeable); the
  .conf download is the path to an automatic name (filename = tunnel name).

Tests: header sums live peer counters. 354 pass, Pint clean, /wireguard 200 with
no console errors, lang parity 89/89.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 22:59:26 +02:00
boban 60e1edc0d5 feat(wg): configurable DNS + Server/Peers tabs + faster offline (v0.9.45)
Three things the operator asked for on the WireGuard page:

- DNS is no longer hardcoded to 1.1.1.1. New host action set-dns (_set_dns +
  cmd_set_dns + write-bridge branch + WgBridge validation, IPv4 list only),
  stored as WG_DNS in wg.env (appended for pre-existing tunnels), surfaced via
  the collector + WgStatus, baked into new peer configs (DNS = ${WG_DNS:-1.1.1.1}).
  Editable in the Server tab; e.g. point clients at your own gateway.
- The page is split into two independent tabs: "Peers" (list / add / remove /
  traffic) and "Server configuration" (endpoint / DNS / port / subnet / gate /
  SSH lock + server identity). #[Url] $tab makes it deep-linkable (?tab=server).
- Offline detection: ONLINE_WITHIN 180s -> 150s (PersistentKeepalive=25 keeps
  active peers re-handshaking well within it, so no flapping; a disconnect now
  shows offline ~2.5 min sooner). Endpoint hint clarified: the port is optional
  (auto-appended), no need to repeat the listen port.

Tests: set-dns (write+audit, reject), tab switch/clamp, dns in status; the two
configured-render tests now switch to the server tab for server content. 353
pass, shellcheck clean, Pint clean, both tabs load 200 with no console errors,
lang parity 89/89.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 22:48:46 +02:00
boban 920dfb02bf feat(wg): compact QR + meaningful download filename (v0.9.44)
- QR: 0.9.42 over-enlarged it; bring the show-once peer QR back to a balanced
  ~240px (still crispEdges on a white quiet-zone frame) so it scans on a phone
  without dominating the modal.
- Download filename now becomes a useful tunnel name on import: build it from
  the endpoint host + peer name (e.g. "10.10.90.165-laptop.conf") instead of the
  generic "clusev-wireguard.conf" — WireGuard apps name a tunnel after the file.
  A QR scan can't carry a name (the app prompts for one); the import hint now
  explains both paths.

Tests: download-filename cases (host+peer, fallback); 349 pass, Pint clean,
/wireguard loads 200, lang parity 83/83.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 22:25:41 +02:00
boban dc6d2fae59 fix(wg): endpoint auto-gets the listen port when given as a bare host (v0.9.43)
If the operator entered an endpoint without a port (e.g. "10.10.90.165"
instead of "10.10.90.165:51820"), that value was written verbatim into every
peer config. WireGuard requires the endpoint as host:port, so the WireGuard app
rejected the QR/file import as "keine gültige WireGuard-Konfiguration" — the
real cause of the invalid-config errors (the QR-size fix in 0.9.42 was secondary).

Normalise host-side: append the listen port when the endpoint has no colon, in
both _write_server_conf (setup — covers the dashboard and interactive paths) and
_set_endpoint (later changes). Verified: bare host -> host:port, host:port left
as-is, DNS host -> host:port; write-bridge smoke test still passes; shellcheck
clean. setup_endpoint_hint (de/en) now documents the host:port format.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 22:11:25 +02:00
boban 9bb9f2561b fix(wg): peer QR is now scannable on a phone (v0.9.42)
The show-once peer config QR rendered at 176px (~2.7px per module) with
anti-aliasing on. A WireGuard config QR is dense (~57 modules), so a phone
camera misread it and the WireGuard app rejected the result as "keine gültige
WireGuard-Konfiguration". Render it large (up to ~300px) on a white framed
quiet zone with shape-rendering:crispEdges, so modules stay sharp and ~4.6px
each. The Download button (.conf import on a PC) remains as the alternative.

Verified: old-vs-new render comparison (2.7 → 4.6 px/module, crisp modules);
Tailwind compiles the arbitrary shape-rendering property; /wireguard loads 200
with no console errors.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 22:06:42 +02:00
boban 7794e98997 fix(wg): write-bridge handler no longer dies before replying (v0.9.41)
The host handler `clusev-wg.sh serve-request` aborted under `set -euo pipefail`
BEFORE writing run/wg-result-<id>.json, so the dashboard waited ~30s and showed
"Keine Antwort vom Host" even though the systemd watcher was running correctly.
The write-bridge had therefore never actually worked on a real deployment.

Two root causes, both fixed:

1. Field extraction: each `id/action/name/endpoint/port/subnet` value is pulled
   with `grep -oE | head | sed`. A field that is empty or absent (an "auto"
   endpoint at setup; gate/add-peer/remove-peer payloads that carry no
   endpoint/port/subnet) makes grep fail → with pipefail the whole pipeline
   fails → `set -e` aborts the handler before _write_result. Appended `|| true`
   to every extraction so a non-match yields "" instead of killing the handler.

2. Action dispatch: gate-up/gate-down/gate-ssh-on/off and set-endpoint/port/
   subnet (and remove-peer) called helpers that run require_setup, which `die`s
   (exit) when the tunnel isn't configured yet — aborting serve-request before
   the reply. Each handler now runs in a subshell `( ... )` so an internal exit
   becomes a caught non-zero and still yields a clean failure result.
   (add-peer/setup were already isolated via their $(...) command substitution.)

Verified end-to-end: all ten actions now write a result for minimal/empty-field
payloads instead of timing out. New smoke test docker/wg/serve-request.test.sh
(root-bypassed, no WireGuard needed) guards both regressions. shellcheck clean,
348 PHP tests pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 21:53:25 +02:00
boban 20aec565ed docs: enterprise README for Gitea — branded hero + architecture diagram
Rebuild the README around the product's "Tactical Terminal" design system
(dark graphite, signal-orange, cyan, mono type) so it reads as a professional,
self-hosted enterprise product instead of a plain text file:

- docs/assets/clusev-banner.svg — hero banner with the Clusev wordmark, tagline,
  brand chips, and a live "fleet" node-graph (control-plane hub → server cards
  with the online/warning/offline status triad + CPU bars and animated data
  pulses). Self-contained: the real product fonts (Chakra Petch + JetBrains Mono)
  are embedded base64 so Gitea's <img>-rendered SVG shows pixel-correct type.
- docs/assets/clusev-architecture.svg — Operator → Control plane → Fleet diagram
  (HTTPS/WS · Reverb in, SSH+SFTP · phpseclib out), same brand language.
- README.md — centered hero, brand-matched shields badges, anchor nav, a
  "why" three-up, a Features table, the architecture diagram + a tech-stack
  table, and the existing install / domain-TLS / updating / security-recovery
  content kept verbatim (now with a WireGuard feature row). All graphics are
  external SVG files (Gitea strips inline <svg>); relative paths resolve on the
  default branch.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 16:52:00 +02:00
boban 9c0d390365 refactor(wireguard): remove dead code + DRY the WG surface (no behavior change)
Verified dead-code + duplication cleanup over the WireGuard feature surface
(found via an audit workflow, every item adversarially re-verified by grep):

Dead code removed:
- lang keys not_configured_title / not_configured_body (both locales) — orphaned
  when the unconfigured empty state became the in-page setup form (setup_title /
  setup_intro). 0 references anywhere. de/en parity kept (83/83).
- WgTraffic::series() 'buckets' return key — written, never read by blade/route/test.
- blade $fmtB byte-formatter — byte-identical duplicate of the top-level $fmtBytes
  already in scope; collapsed onto $fmtBytes.

DRY / readability (behavior-preserving):
- Index: PEER_NAME_RE constant (was the same regex literal at 5 sites), portInRange()
  + clampWindow() helpers (window default now self::WINDOWS[0]), openConfirm() helper
  (5 confirm-modal openers shared one dispatch), onFlag() helper (2 apply handlers).
- WgBridge: validPort() helper (port check was duplicated verbatim).
- clusev-wg.sh: _write_server_conf() shared by both setup paths (the byte-identical
  server bring-up was copy-pasted); single CIDR_RE constant for the 3 subnet checks;
  fixed a stale header comment that still claimed SSH is "NEVER matched" (the v0.9.40
  ssh-lock can match 22); documented the defence-in-depth re-validation in _set_*.
- WgStatus: documented server.pubkey is part of the /wg-status.json contract (kept).

No DB/migration changes (peer_name write-only column left intact — needs a
coordinated migration). 348 tests pass, shellcheck clean, Pint clean, R12 verified
in both the configured + unconfigured states (200, no console errors, no leaked
keys, traffic formatter renders 1.5 MB / 200 KB correctly).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 16:33:41 +02:00
boban 2b245c3d3a feat(wireguard): SSH lock + peer config download (v0.9.40)
Two dashboard additions to the WireGuard page:

- Peer config download: the show-once new-client view gets a "Download"
  button next to the QR code (streams clusev-wireguard.conf) plus an
  inline hint explaining QR-on-phone vs download-on-PC import.

- SSH lock (port 22): an optional toggle in WireGuard settings that walls
  off host SSH to the tunnel via a dedicated CLUSEV-WG-SSH chain on the
  host INPUT chain. Fail-open by construction: ESTABLISHED,RELATED + lo +
  wg0 + WG-subnet RETURN before the DROP (live sessions survive, SSH over
  the tunnel stays open); _ssh_gate_on refuses while wg0 is down; the boot
  unit's ConditionPathExists=wg0 and an in-function self-guard keep a
  broken tunnel from ever applying the DROP; ssh_gate_apply re-validates
  the subnet shape and fails open on a corrupt wg.env. Strong lock-out
  warning shown inline AND in the confirm modal (recommend a backup peer).
  New CLI escapes: clusev wg ssh-lock / ssh-unlock; clusev wg down clears
  both gates. Panel gate and SSH gate are independent toggles.

Write-bridge: new gate-ssh-on/gate-ssh-off actions (no args, host
whitelist + ConfirmToken wgSshGate, single-use uid-bound). Status collector
now reports gate.ssh. Adversarial lock-out review: all 8 failure modes
refuted. 348 tests pass, shellcheck clean, Pint clean, R12 verified.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 09:25:19 +02:00
boban 94425f93e8 fix(wg): remove SSH hint from setup form + time out a non-responding host (no endless spinner)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 08:38:43 +02:00
boban a064e8eeb8 chore: release 0.9.38 — WireGuard first-time setup from the dashboard
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 08:16:40 +02:00
boban 8e05cf62ec feat(wg): set WireGuard up from the dashboard (setup form + bridge action)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-21 08:09:35 +02:00
boban 01778bde80 feat(wg): non-interactive setup action for the dashboard write-bridge
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-21 08:06:00 +02:00
boban 6f9091d3fa docs(wg): SP2 Phase 5 plan — first-time setup from the dashboard
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 08:04:21 +02:00
boban 2d12978616 chore: release 0.9.37 — WireGuard settings + gate from the dashboard (SP2 phase 4, complete)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 01:03:05 +02:00
boban 858a00955a fix(wg): single audit per confirm action (no descriptor double-audit)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 01:00:26 +02:00
boban 7ccefa5890 feat(wg): gate toggle + endpoint/port/subnet settings from the dashboard
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-21 00:53:29 +02:00
boban 7ff02c4e68 feat(wg): set-endpoint/set-port/set-subnet host actions + serve-request branches 2026-06-21 00:49:55 +02:00
boban 95a5811544 docs(wg): SP2 Phase 4 plan — settings + gate from the UI
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 00:48:11 +02:00
boban e006de4962 chore: release 0.9.36 — WireGuard peer management from the dashboard (SP2 phase 3)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 00:45:10 +02:00
boban b1387bd654 fix(wg): umask 077 in serve-request + UMask in unit — no 0644 race on key files (security review)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 00:42:07 +02:00
boban ee8f2bac91 feat(wg): add/remove peers from the dashboard (show-once config + QR)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-21 00:36:26 +02:00
boban ed5506e558 feat(wg): WgBridge — app side of the write-bridge (validate + request + result) 2026-06-21 00:32:02 +02:00
boban a6bb8b7da4 feat(wg): clusev-wg-request watcher (path+service) + install wiring
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 00:30:12 +02:00
boban 695b33cf37 feat(wg): serve-request write-bridge + peer/gate cores (host-side validation)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 00:28:34 +02:00
boban f91d8ed834 docs(wg): SP2 Phase 3 plan — peer management + write-bridge
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 00:24:44 +02:00
boban 037d285148 chore: release 0.9.35 — WireGuard traffic history + scheduler (SP2 phase 2)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 00:13:30 +02:00