fix: dedupe concurrent gateway restarts + surface restart outcome in onboarding UI

Follow-ups to the salvaged Telegram QR onboarding auto-restart:

- _spawn_gateway_restart() reuses a live in-flight 'hermes gateway restart'
  child instead of spawning a second racing one (stale cached frontend +
  new backend both requesting a restart, or restart-button double-click).
  Both /api/gateway/restart and the onboarding apply path go through it.
- ChannelsPage polls /api/actions/gateway-restart/status after a
  server-initiated restart and surfaces a non-zero exit (e.g. systemd
  linger missing) via the manual-restart banner, since restart_started
  only means the child spawned.
- Test for the reuse path + _ACTION_PROCS isolation in existing tests.
This commit is contained in:
teknium1 2026-06-10 01:14:16 -07:00 committed by Teknium
parent 984e69ff62
commit fa32af886f
3 changed files with 112 additions and 2 deletions

View file

@ -599,6 +599,32 @@ function TelegramOnboardingPanel({
setNewAllowedId("");
};
// restart_started only means the `hermes gateway restart` child spawned —
// not that the restart will succeed (e.g. systemd linger missing, service
// manager failure). Poll the action status briefly and surface a non-zero
// exit via the manual-restart banner. Note: in no-service installs the
// child becomes the foreground gateway and never exits, so "still running
// when the window closes" counts as success.
const watchRestartOutcome = async () => {
for (let i = 0; i < 20; i++) {
await new Promise((resolve) => setTimeout(resolve, 1500));
try {
const st = await api.getActionStatus("gateway-restart", 5);
if (st.running) continue;
if (st.exit_code !== 0 && st.exit_code !== null) {
onRestartNeeded();
showToast(
`Gateway restart failed (exit ${st.exit_code}) — restart manually`,
"error",
);
}
return;
} catch {
// transient fetch error; keep polling
}
}
};
const apply = async () => {
if (!setup) return;
if (allowedIds.length === 0) {
@ -616,6 +642,7 @@ function TelegramOnboardingPanel({
showToast("Telegram saved; gateway restarting…", "success");
setRestartNeeded(false);
setTimeout(() => void onChanged(), 4000);
void watchRestartOutcome();
} else if (result.restart_started === undefined && result.needs_restart) {
try {
await api.restartGateway();