fix: auto-updater proxy drop + protect internal docs from git

Auto-update POST goes through Next.js proxy which dies when extracted
files trigger hot-reload. Network drops now transition to restart polling
instead of showing failure. Also adds admin key header and FastAPI error
field fallback. Gitignore updated to protect internal docs.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

Former-commit-id: 03162f8a4b7ad8a0f2983f81361df7dba42a8689
This commit is contained in:
anoracleofra-code
2026-03-14 14:18:30 -06:00
parent 90c2e90e2c
commit 8ff4516a7a
2 changed files with 50 additions and 26 deletions
+7
View File
@@ -106,3 +106,10 @@ jobs.json
.claude
.mise.local.toml
# Local-only internal docs (never commit)
.local-docs/
ROADMAP.md
UPDATEPROTOCOL.md
CLAUDE.md
DOCKER_SECRETS.md
+43 -26
View File
@@ -57,39 +57,56 @@ export default function TopRightControls() {
}
};
const startRestartPolling = () => {
setUpdateStatus("restarting");
// Poll /api/health until backend comes back
pollRef.current = setInterval(async () => {
try {
const h = await fetch(`${API_BASE}/api/health`);
if (h.ok) {
if (pollRef.current) clearInterval(pollRef.current);
if (timeoutRef.current) clearTimeout(timeoutRef.current);
window.location.reload();
}
} catch {
// Backend still down — keep polling
}
}, 3000);
// Give up after 90 seconds
timeoutRef.current = setTimeout(() => {
if (pollRef.current) clearInterval(pollRef.current);
setErrorMessage("Restart timed out — the app may need to be started manually.");
setUpdateStatus("update_error");
}, 90000);
};
const triggerUpdate = async () => {
setUpdateStatus("updating");
setErrorMessage("");
try {
const res = await fetch(`${API_BASE}/api/system/update`, { method: "POST" });
const headers: Record<string, string> = {};
const adminKey = typeof window !== "undefined" ? localStorage.getItem("sb_admin_key") : null;
if (adminKey) headers["X-Admin-Key"] = adminKey;
const res = await fetch(`${API_BASE}/api/system/update`, { method: "POST", headers });
const data = await res.json();
if (!res.ok) throw new Error(data.message || "Update failed");
if (!res.ok) throw new Error(data.message || data.detail || "Update failed");
setUpdateStatus("restarting");
// Poll /api/health until backend comes back
pollRef.current = setInterval(async () => {
try {
const h = await fetch(`${API_BASE}/api/health`);
if (h.ok) {
if (pollRef.current) clearInterval(pollRef.current);
if (timeoutRef.current) clearTimeout(timeoutRef.current);
window.location.reload();
}
} catch {
// Backend still down — keep polling
}
}, 3000);
// Give up after 90 seconds
timeoutRef.current = setTimeout(() => {
if (pollRef.current) clearInterval(pollRef.current);
setErrorMessage("Restart timed out — the app may need to be started manually.");
setUpdateStatus("update_error");
}, 90000);
startRestartPolling();
} catch (err: any) {
setErrorMessage(err.message || "Unknown error");
setUpdateStatus("update_error");
// The update extracts files over the project, which causes the Next.js
// dev server to hot-reload and drop the proxy connection mid-request.
// A network error during update likely means it SUCCEEDED and the
// server is restarting — transition to polling instead of showing failure.
const isNetworkDrop = err instanceof TypeError || err.message === "Failed to fetch";
if (isNetworkDrop) {
startRestartPolling();
} else {
setErrorMessage(err.message || "Unknown error");
setUpdateStatus("update_error");
}
}
};