fix: добавить 15-секундный таймаут fetch во все модули (measurements, assembly, proposals, request)

Кнопки больше не зависают бесконечно при медленном или недоступном бэкенде.
AbortController + дружелюбное сообщение «Сервер не отвечает — попробуйте ещё раз».

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
wasrusgen 2026-05-18 09:27:53 +03:00
parent 40e2275949
commit 0fb0597b8d
5 changed files with 81 additions and 64 deletions

View File

@ -17,6 +17,20 @@ const Assembly = (function () {
manager_note: "",
};
async function _fetchWithTimeout(url, body, timeoutMs = 15000) {
const ctrl = new AbortController();
const timer = setTimeout(() => ctrl.abort(), timeoutMs);
try {
const res = await fetch(url, { method: "POST", signal: ctrl.signal, body: JSON.stringify(body) });
return await res.json();
} catch (e) {
if (e.name === "AbortError") throw new Error("Сервер не отвечает — попробуйте ещё раз");
throw e;
} finally {
clearTimeout(timer);
}
}
function mount(container) {
root = container;
document.body.classList.remove("has-bottom-nav");
@ -198,11 +212,7 @@ const Assembly = (function () {
};
try {
const res = await fetch(`${BACKEND_URL}/api/assembly_create`, {
method: "POST",
body: JSON.stringify(body),
});
const data = await res.json();
const data = await _fetchWithTimeout(`${BACKEND_URL}/api/assembly_create`, body);
if (data.error) {
result.innerHTML = `<div class="error">Ошибка: ${escHtml(data.error)}</div>`;
btn.disabled = false;
@ -247,14 +257,10 @@ const Assembly = (function () {
const loading = el(`<div class="loader-inline"><div class="spinner"></div></div>`);
root.appendChild(loading);
try {
const res = await fetch(`${BACKEND_URL}/api/assembly_list`, {
method: "POST",
body: JSON.stringify({
const data = await _fetchWithTimeout(`${BACKEND_URL}/api/assembly_list`, {
initData: tg?.initData || "",
initDataUnsafe: tg?.initDataUnsafe || null,
}),
});
const data = await res.json();
});
loading.remove();
if (data.error) {
root.appendChild(el(`<div class="error">${escHtml(data.error)}</div>`));
@ -309,15 +315,11 @@ const Assembly = (function () {
root.appendChild(loading);
let a;
try {
const res = await fetch(`${BACKEND_URL}/api/assembly_detail`, {
method: "POST",
body: JSON.stringify({
a = await _fetchWithTimeout(`${BACKEND_URL}/api/assembly_detail`, {
initData: tg?.initData || "",
initDataUnsafe: tg?.initDataUnsafe || null,
assembly_id: id,
}),
});
a = await res.json();
});
} catch (e) {
loading.remove();
root.appendChild(el(`<div class="error">Сеть: ${escHtml(e.message)}</div>`));

View File

@ -94,20 +94,30 @@ const Measurements = (function () {
render();
}
async function _fetchWithTimeout(url, body, timeoutMs = 15000) {
const ctrl = new AbortController();
const timer = setTimeout(() => ctrl.abort(), timeoutMs);
try {
const res = await fetch(url, { method: "POST", signal: ctrl.signal, body: JSON.stringify(body) });
return await res.json();
} catch (e) {
if (e.name === "AbortError") throw new Error("Сервер не отвечает — попробуйте ещё раз");
throw e;
} finally {
clearTimeout(timer);
}
}
async function loadRequestAndStart() {
root.innerHTML = "";
root.appendChild(renderHeader("Закрыть заявку"));
root.appendChild(el(`<div class="loader-inline"><div class="spinner"></div></div>`));
try {
const res = await fetch(`${BACKEND_URL}/api/measurement_detail`, {
method: "POST",
body: JSON.stringify({
const data = await _fetchWithTimeout(`${BACKEND_URL}/api/measurement_detail`, {
initData: tg?.initData || "",
initDataUnsafe: tg?.initDataUnsafe || null,
measurement_id: measurementId,
}),
});
const data = await res.json();
});
if (data.error) {
root.innerHTML = "";
root.appendChild(renderHeader("Ошибка"));
@ -332,14 +342,10 @@ const Measurements = (function () {
async function fetchNextZamerNo(node) {
try {
const res = await fetch(`${BACKEND_URL}/api/measurement_next_no`, {
method: "POST",
body: JSON.stringify({
const data = await _fetchWithTimeout(`${BACKEND_URL}/api/measurement_next_no`, {
initData: tg?.initData || "",
initDataUnsafe: tg?.initDataUnsafe || null,
}),
});
const data = await res.json();
});
const hint = node.querySelector("#zamerNoHint");
const input = node.querySelector("#zamerNoInput");
if (data.ok && data.next_no && input && !state.zamer_no) {
@ -529,14 +535,10 @@ const Measurements = (function () {
let clients = [];
try {
const res = await fetch(`${BACKEND_URL}/api/clients`, {
method: "POST",
body: JSON.stringify({
const data = await _fetchWithTimeout(`${BACKEND_URL}/api/clients`, {
initData: tg?.initData || "",
initDataUnsafe: tg?.initDataUnsafe || null,
}),
});
const data = await res.json();
});
clients = (data.clients || []).sort((a, b) =>
(a.client_name || "").localeCompare(b.client_name || "", "ru")
);
@ -937,15 +939,11 @@ const Measurements = (function () {
};
try {
const res = await fetch(`${BACKEND_URL}/api/measurement`, {
method: "POST",
body: JSON.stringify({
const data = await _fetchWithTimeout(`${BACKEND_URL}/api/measurement`, {
initData: tg?.initData || "",
initDataUnsafe: tg?.initDataUnsafe || null,
measurement,
}),
});
const data = await res.json();
});
if (data.error) {
result.innerHTML = `<div class="error">Ошибка: ${data.error}</div>`;
btn.disabled = false; btn.textContent = isUpdate ? "Закрыть заявку" : "Сохранить замер";

View File

@ -19,13 +19,23 @@ const Proposals = (function () {
return { initData: tg?.initData || "", initDataUnsafe: tg?.initDataUnsafe || null };
}
async function apiFetch(path, extra = {}) {
const res = await fetch(`${BACKEND_URL}/api/${path}`, {
method: "POST",
body: JSON.stringify({ ...authBody(), ...extra }),
});
if (!res.ok) throw new Error("HTTP " + res.status);
return res.json();
async function apiFetch(path, extra = {}, timeoutMs = 15000) {
const ctrl = new AbortController();
const timer = setTimeout(() => ctrl.abort(), timeoutMs);
try {
const res = await fetch(`${BACKEND_URL}/api/${path}`, {
method: "POST",
signal: ctrl.signal,
body: JSON.stringify({ ...authBody(), ...extra }),
});
if (!res.ok) throw new Error("HTTP " + res.status);
return res.json();
} catch (e) {
if (e.name === "AbortError") throw new Error("Сервер не отвечает — попробуйте ещё раз");
throw e;
} finally {
clearTimeout(timer);
}
}
// ── Constants ─────────────────────────────────────────────

View File

@ -15,6 +15,20 @@ const MeasurementRequest = (function () {
};
let measurers = [];
async function _fetchWithTimeout(url, body, timeoutMs = 15000) {
const ctrl = new AbortController();
const timer = setTimeout(() => ctrl.abort(), timeoutMs);
try {
const res = await fetch(url, { method: "POST", signal: ctrl.signal, body: JSON.stringify(body) });
return await res.json();
} catch (e) {
if (e.name === "AbortError") throw new Error("Сервер не отвечает — попробуйте ещё раз");
throw e;
} finally {
clearTimeout(timer);
}
}
function mount(container) {
root = container;
document.body.classList.remove("has-bottom-nav");
@ -116,11 +130,9 @@ const MeasurementRequest = (function () {
async function loadMeasurers() {
try {
const res = await fetch(`${BACKEND_URL}/api/staff_list`, {
method: "POST",
body: JSON.stringify({ initData: tg?.initData || "", role: "measurer" }),
});
const data = await res.json();
const data = await _fetchWithTimeout(`${BACKEND_URL}/api/staff_list`, {
initData: tg?.initData || "", role: "measurer",
});
measurers = data.staff || [];
const sel = document.getElementById("measurerSelect");
const hint = document.getElementById("measurerHint");
@ -163,21 +175,16 @@ const MeasurementRequest = (function () {
result.innerHTML = "";
try {
const res = await fetch(`${BACKEND_URL}/api/measurement_request`, {
method: "POST",
body: JSON.stringify({
const data = await _fetchWithTimeout(`${BACKEND_URL}/api/measurement_request`, {
initData: tg?.initData || "",
initDataUnsafe: tg?.initDataUnsafe || null,
client_name: name,
client_phone: phone,
address: state.address || "",
assigned_to_tg_id: state.assigned_to_tg_id || "",
// Примечание (рекомендации по дате + особенности) — единое поле
preferred_note: state.preferred_note || "",
preferred_type: "tbd",
}),
});
const data = await res.json();
});
if (data.error) {
result.innerHTML = `<div class="error">Ошибка: ${data.error}</div>`;
btn.disabled = false;

View File

@ -41,10 +41,10 @@
<script src="assets/podbor.js?v=20260517d"></script>
<script src="assets/clients.js?v=20260518e"></script>
<script src="assets/zamer-picts.js?v=20260516h"></script>
<script src="assets/measurements.js?v=20260517d"></script>
<script src="assets/request.js?v=20260517d"></script>
<script src="assets/assembly.js?v=20260517d"></script>
<script src="assets/proposals.js?v=20260516h"></script>
<script src="assets/measurements.js?v=20260518f"></script>
<script src="assets/request.js?v=20260518f"></script>
<script src="assets/assembly.js?v=20260518f"></script>
<script src="assets/proposals.js?v=20260518f"></script>
<script src="assets/app.js?v=20260517d"></script>
</body>
</html>