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>feat/v1-foundation v0.9.44
parent
dc6d2fae59
commit
920dfb02bf
12
CHANGELOG.md
12
CHANGELOG.md
|
|
@ -13,7 +13,17 @@ getaggte Releases (Kanal `stable`, optional `beta`) — niemals Entwicklungs-Bui
|
|||
|
||||
_Keine offenen Änderungen — der nächste Stand wird hier gesammelt und als `vX.Y.Z` getaggt._
|
||||
|
||||
## [0.9.43] - 2026-06-21
|
||||
## [0.9.44] - 2026-06-21
|
||||
|
||||
### Geändert
|
||||
- **Peer-QR-Code kompakter.** Nach der Vergrößerung in 0.9.42 war der QR zu groß; er ist jetzt auf
|
||||
ein ausgewogenes Maß (~240 px) reduziert — weiterhin pixelgenau (`crispEdges`) auf weißem Rahmen,
|
||||
damit das Scannen am Handy zuverlässig bleibt.
|
||||
- **Download-Dateiname als Tunnel-Name.** Die heruntergeladene Client-Config heißt jetzt
|
||||
`<endpoint-host>-<peer>.conf` (z. B. `10.10.90.165-laptop.conf`) statt `clusev-wireguard.conf`.
|
||||
WireGuard-Apps benennen einen Tunnel nach der Datei, sodass der Tunnel sinnvoll vorbenannt ist.
|
||||
(Beim QR-Scan fragt die WireGuard-App selbst nach einem Namen — das gibt der QR-Standard nicht her;
|
||||
ein Hinweis im Einmal-Dialog erklärt beide Wege.)
|
||||
|
||||
### Behoben
|
||||
- **Endpoint ohne Port machte jede Client-Config ungültig.** Wurde beim Einrichten/Ändern nur ein
|
||||
|
|
|
|||
|
|
@ -59,6 +59,11 @@ class Index extends Component
|
|||
|
||||
public ?string $resultQr = null;
|
||||
|
||||
/** Peer name of the in-flight request, carried to the result so the download gets a meaningful filename. */
|
||||
public ?string $pendingName = null;
|
||||
|
||||
public ?string $resultName = null;
|
||||
|
||||
public function setWindow(int $seconds): void
|
||||
{
|
||||
$this->window = $this->clampWindow($seconds);
|
||||
|
|
@ -76,6 +81,7 @@ class Index extends Component
|
|||
$name = $this->newPeer;
|
||||
$this->pendingId = $bridge->request('add-peer', ['name' => $name]);
|
||||
$this->pendingAction = 'add-peer';
|
||||
$this->pendingName = $name;
|
||||
$this->newPeer = '';
|
||||
$this->audit('wg.add-peer', $name);
|
||||
}
|
||||
|
|
@ -106,6 +112,7 @@ class Index extends Component
|
|||
'endpoint' => $this->setupEndpoint, 'name' => $this->setupPeer,
|
||||
]);
|
||||
$this->pendingAction = 'setup';
|
||||
$this->pendingName = $this->setupPeer;
|
||||
$this->audit('wg.setup', $this->setupSubnet);
|
||||
}
|
||||
|
||||
|
|
@ -324,6 +331,7 @@ class Index extends Component
|
|||
if ($res['ok'] && in_array($this->pendingAction, ['add-peer', 'setup'], true) && $res['config'] !== null) {
|
||||
$this->resultConfig = $res['config'];
|
||||
$this->resultQr = $res['qr'];
|
||||
$this->resultName = $this->pendingName;
|
||||
} elseif (! $res['ok']) {
|
||||
$msg = ($res['message'] ?? '') !== '' ? $res['message'] : __('wireguard.action_failed');
|
||||
$this->dispatch('notify', message: $msg, level: 'error');
|
||||
|
|
@ -331,15 +339,21 @@ class Index extends Component
|
|||
$this->dispatch('notify', message: __('wireguard.action_done'));
|
||||
}
|
||||
$this->pendingAction = null;
|
||||
$this->pendingName = null;
|
||||
}
|
||||
|
||||
public function dismissResult(): void
|
||||
{
|
||||
$this->resultConfig = null;
|
||||
$this->resultQr = null;
|
||||
$this->resultName = null;
|
||||
}
|
||||
|
||||
/** Download the show-once client config as a .conf file (for importing on a PC). */
|
||||
/**
|
||||
* Download the show-once client config as a .conf file. The filename becomes the tunnel name when
|
||||
* imported (WireGuard apps name a tunnel after the file), so build it from the endpoint host + peer
|
||||
* name — e.g. "10.10.90.165-laptop.conf". (A QR scan can't carry a name; the app prompts for one.)
|
||||
*/
|
||||
public function downloadConfig(): ?StreamedResponse
|
||||
{
|
||||
if ($this->resultConfig === null) {
|
||||
|
|
@ -347,9 +361,14 @@ class Index extends Component
|
|||
}
|
||||
$cfg = $this->resultConfig;
|
||||
|
||||
$host = preg_match('/^Endpoint\s*=\s*([^:\s]+)/m', $cfg, $m) === 1 ? $m[1] : 'clusev';
|
||||
$parts = array_filter([$host, (string) $this->resultName]);
|
||||
$base = preg_replace('/[^A-Za-z0-9._-]+/', '-', implode('-', $parts));
|
||||
$base = trim((string) $base, '-.') ?: 'clusev-wireguard';
|
||||
|
||||
return response()->streamDownload(function () use ($cfg) {
|
||||
echo $cfg;
|
||||
}, 'clusev-wireguard.conf', ['Content-Type' => 'text/plain; charset=UTF-8']);
|
||||
}, $base.'.conf', ['Content-Type' => 'text/plain; charset=UTF-8']);
|
||||
}
|
||||
|
||||
/** Clamp a traffic window to one of the allowed values, falling back to the shortest. */
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
return [
|
||||
// First tagged release is v0.1.0 (semantic, not -dev).
|
||||
'version' => '0.9.43',
|
||||
'version' => '0.9.44',
|
||||
|
||||
// Deployed commit + branch. install.sh bakes these into .env from the host's .git (the prod
|
||||
// image ships no .git); the Versions page prefers them and falls back to a live .git read in
|
||||
|
|
|
|||
|
|
@ -45,7 +45,7 @@ return [
|
|||
'new_peer_once' => 'Diese Konfiguration wird NUR EINMAL angezeigt und nicht gespeichert. Jetzt importieren (QR scannen oder Text kopieren).',
|
||||
'close' => 'Schließen',
|
||||
'download' => 'Herunterladen',
|
||||
'import_hint' => 'QR-Code mit der WireGuard-App scannen (Handy) oder Konfiguration herunterladen und importieren (PC).',
|
||||
'import_hint' => 'QR mit der WireGuard-App scannen (Handy; die App fragt nach einem Tunnel-Namen) oder herunterladen und importieren (PC) — der Dateiname wird zum Tunnel-Namen.',
|
||||
'settings_title' => 'Einstellungen',
|
||||
'apply' => 'Anwenden',
|
||||
'gate_toggle' => 'Gate',
|
||||
|
|
|
|||
|
|
@ -45,7 +45,7 @@ return [
|
|||
'new_peer_once' => 'This configuration is shown ONCE and never stored. Import it now (scan the QR or copy the text).',
|
||||
'close' => 'Close',
|
||||
'download' => 'Download',
|
||||
'import_hint' => 'Scan the QR code with the WireGuard app (phone) or download the configuration and import it (PC).',
|
||||
'import_hint' => 'Scan the QR with the WireGuard app (phone; it asks for a tunnel name) or download and import it (PC) — the filename becomes the tunnel name.',
|
||||
'settings_title' => 'Settings',
|
||||
'apply' => 'Apply',
|
||||
'gate_toggle' => 'Gate',
|
||||
|
|
|
|||
|
|
@ -273,9 +273,9 @@
|
|||
<p class="mt-1 text-sm text-warning">{{ __('wireguard.new_peer_once') }}</p>
|
||||
<p class="mt-1 font-mono text-[11px] text-ink-4">{{ __('wireguard.import_hint') }}</p>
|
||||
@if ($resultQr)
|
||||
{{-- White frame + generous size + crispEdges: a WireGuard config QR is dense (~57
|
||||
modules); too small or anti-aliased and a phone camera misreads it. --}}
|
||||
<div class="mx-auto mt-4 w-full max-w-[20rem] rounded-lg bg-white p-3">
|
||||
{{-- White frame + crispEdges: a WireGuard config QR is dense (~57 modules); too
|
||||
small or anti-aliased and a phone camera misreads it. --}}
|
||||
<div class="mx-auto mt-4 w-full max-w-[15rem] rounded-lg bg-white p-3">
|
||||
<div class="aspect-square w-full [&>svg]:h-full [&>svg]:w-full [&>svg]:[shape-rendering:crispEdges]">{!! $resultQr !!}</div>
|
||||
</div>
|
||||
@endif
|
||||
|
|
|
|||
|
|
@ -205,12 +205,22 @@ class WireguardPageTest extends TestCase
|
|||
->assertHasErrors('setupSubnet');
|
||||
}
|
||||
|
||||
public function test_download_config_streams_a_conf_file(): void
|
||||
public function test_download_config_filename_uses_endpoint_host_and_peer_name(): void
|
||||
{
|
||||
// The .conf filename becomes the tunnel name on import — build it from endpoint host + peer.
|
||||
Livewire::test(Index::class)
|
||||
->set('resultConfig', "[Interface]\nPrivateKey = x\n\n[Peer]\nEndpoint = 10.10.90.165:51820")
|
||||
->set('resultName', 'laptop')
|
||||
->call('downloadConfig')
|
||||
->assertFileDownloaded('10.10.90.165-laptop.conf');
|
||||
}
|
||||
|
||||
public function test_download_config_falls_back_to_a_default_filename(): void
|
||||
{
|
||||
Livewire::test(Index::class)
|
||||
->set('resultConfig', "[Interface]\nPrivateKey = x")
|
||||
->call('downloadConfig')
|
||||
->assertFileDownloaded('clusev-wireguard.conf');
|
||||
->assertFileDownloaded('clusev.conf');
|
||||
}
|
||||
|
||||
public function test_poll_result_times_out_when_the_host_does_not_respond(): void
|
||||
|
|
|
|||
Loading…
Reference in New Issue