feat: pin TCP/QUIC listen ports + visible error banner
Two related improvements requested while testing the private fork: 1. Custom TCP/QUIC listen ports (SidecarConfig.tcpListenPort, .quicListenPort). With ephemeral ports, the daemon's peer-listen port changes at every restart, which makes port-forwarding on a home router useless after the first daemon stop. Pinning these keeps the same port across restarts so other private-network nodes can dial in reliably. Backend uses the configured port if Some, falls back to pick_port_skip otherwise. Frontend exposes two inputs in Settings → Daemon configuration with a help line explaining when to fill them. 2. Daemon-failure banner in App.vue. The previous behaviour was silent: a click on \"Start daemon\" that hit a backend error only flipped the sidebar dot to red, with no message visible. Now an inline banner at the top of the main content area shows the error, plus a \"Go to Settings\" shortcut when the message mentions a network config issue.
This commit is contained in:
16
src/App.vue
16
src/App.vue
@@ -120,6 +120,22 @@ async function handleStop() {
|
||||
<header class="flex h-14 items-center border-b border-border px-6 shrink-0">
|
||||
<h1 class="text-lg font-semibold">{{ currentTitle }}</h1>
|
||||
</header>
|
||||
<div
|
||||
v-if="phase === 'error' && error"
|
||||
class="border-b border-destructive/40 bg-destructive/10 px-6 py-3"
|
||||
>
|
||||
<div class="flex items-start gap-3 text-sm">
|
||||
<span class="font-medium text-destructive">Daemon failed to start —</span>
|
||||
<span class="flex-1 break-words">{{ error }}</span>
|
||||
<button
|
||||
v-if="error.toLowerCase().includes('network')"
|
||||
class="shrink-0 rounded-md border border-destructive px-2 py-0.5 text-xs text-destructive hover:bg-destructive hover:text-destructive-foreground"
|
||||
@click="$router.push('/settings')"
|
||||
>
|
||||
Go to Settings
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-1 overflow-y-auto p-6">
|
||||
<RouterView />
|
||||
</div>
|
||||
|
||||
@@ -23,6 +23,10 @@ export interface SidecarConfig {
|
||||
/** UTF-8, 2..=64 bytes. Public — must match across all nodes of the
|
||||
* same private overlay. */
|
||||
networkName: string | null;
|
||||
/** Pin TCP/QUIC listen ports so they survive a daemon restart and can
|
||||
* be port-forwarded reliably. `null` ⇒ ephemeral port at each start. */
|
||||
tcpListenPort: number | null;
|
||||
quicListenPort: number | null;
|
||||
}
|
||||
|
||||
export interface NetworkKeyStatus {
|
||||
|
||||
@@ -14,8 +14,17 @@ const DEFAULT_CONFIG: SidecarConfig = {
|
||||
tunName: null,
|
||||
noTun: false,
|
||||
networkName: null,
|
||||
tcpListenPort: null,
|
||||
quicListenPort: null,
|
||||
};
|
||||
|
||||
function normalizePort(p: unknown): number | null {
|
||||
if (p === null || p === undefined || p === "" || p === 0) return null;
|
||||
const n = typeof p === "number" ? p : Number(p);
|
||||
if (!Number.isInteger(n) || n < 1 || n > 65535) return null;
|
||||
return n;
|
||||
}
|
||||
|
||||
export const useConfigStore = defineStore("config", () => {
|
||||
const config = ref<SidecarConfig>({ ...DEFAULT_CONFIG });
|
||||
const loaded = ref(false);
|
||||
@@ -35,6 +44,8 @@ export const useConfigStore = defineStore("config", () => {
|
||||
tunName: saved.tunName ?? null,
|
||||
noTun: !!saved.noTun,
|
||||
networkName: saved.networkName ?? null,
|
||||
tcpListenPort: normalizePort(saved.tcpListenPort),
|
||||
quicListenPort: normalizePort(saved.quicListenPort),
|
||||
};
|
||||
}
|
||||
loaded.value = true;
|
||||
@@ -46,6 +57,8 @@ export const useConfigStore = defineStore("config", () => {
|
||||
tunName: next.tunName?.trim() ? next.tunName.trim() : null,
|
||||
noTun: !!next.noTun,
|
||||
networkName: next.networkName?.trim() ? next.networkName.trim() : null,
|
||||
tcpListenPort: normalizePort(next.tcpListenPort),
|
||||
quicListenPort: normalizePort(next.quicListenPort),
|
||||
};
|
||||
const s = await ensureStore();
|
||||
await s.set(KEY, config.value);
|
||||
|
||||
@@ -30,6 +30,8 @@ const draft = reactive({
|
||||
tunName: "",
|
||||
noTun: false,
|
||||
networkName: "",
|
||||
tcpListenPort: "",
|
||||
quicListenPort: "",
|
||||
});
|
||||
const dirty = ref(false);
|
||||
|
||||
@@ -38,9 +40,19 @@ function loadDraft() {
|
||||
draft.tunName = config.value.tunName ?? "";
|
||||
draft.noTun = config.value.noTun;
|
||||
draft.networkName = config.value.networkName ?? "";
|
||||
draft.tcpListenPort = config.value.tcpListenPort?.toString() ?? "";
|
||||
draft.quicListenPort = config.value.quicListenPort?.toString() ?? "";
|
||||
dirty.value = false;
|
||||
}
|
||||
|
||||
function parsePort(s: string): number | null {
|
||||
const t = s.trim();
|
||||
if (!t) return null;
|
||||
const n = Number(t);
|
||||
if (!Number.isInteger(n) || n < 1 || n > 65535) return null;
|
||||
return n;
|
||||
}
|
||||
|
||||
watch(config, loadDraft, { immediate: true });
|
||||
|
||||
watch(
|
||||
@@ -60,6 +72,8 @@ async function save() {
|
||||
tunName: draft.tunName.trim() || null,
|
||||
noTun: draft.noTun,
|
||||
networkName: draft.networkName.trim() || null,
|
||||
tcpListenPort: parsePort(draft.tcpListenPort),
|
||||
quicListenPort: parsePort(draft.quicListenPort),
|
||||
});
|
||||
dirty.value = false;
|
||||
}
|
||||
@@ -387,6 +401,40 @@ onMounted(async () => {
|
||||
:disabled="draft.noTun"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-3 sm:grid-cols-2">
|
||||
<div>
|
||||
<label class="text-xs uppercase tracking-wide text-muted-foreground">
|
||||
TCP listen port
|
||||
</label>
|
||||
<input
|
||||
v-model="draft.tcpListenPort"
|
||||
type="text"
|
||||
inputmode="numeric"
|
||||
class="mt-1 w-full rounded-md border border-input bg-background px-3 py-1.5 font-mono text-sm focus:outline-none focus:ring-2 focus:ring-ring"
|
||||
placeholder="ephemeral"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label class="text-xs uppercase tracking-wide text-muted-foreground">
|
||||
QUIC listen port
|
||||
</label>
|
||||
<input
|
||||
v-model="draft.quicListenPort"
|
||||
type="text"
|
||||
inputmode="numeric"
|
||||
class="mt-1 w-full rounded-md border border-input bg-background px-3 py-1.5 font-mono text-sm focus:outline-none focus:ring-2 focus:ring-ring"
|
||||
placeholder="ephemeral"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<p class="text-[11px] text-muted-foreground">
|
||||
Pin these to a fixed value (e.g. <code class="font-mono">9651</code> for
|
||||
TCP, <code class="font-mono">9652</code> for QUIC) when you port-forward
|
||||
a stable port on your router so other private-network nodes can dial in
|
||||
across daemon restarts. Leave empty to keep using a different ephemeral
|
||||
port at every start.
|
||||
</p>
|
||||
<div class="flex justify-end gap-2 pt-2">
|
||||
<button
|
||||
class="inline-flex items-center gap-1.5 rounded-md border border-border px-3 py-1.5 text-xs hover:bg-secondary"
|
||||
|
||||
Reference in New Issue
Block a user