Фронтенд: стабильность прокси и рендера

- main.py: таймаут прокси 150с (холодная загрузка бэкенда до ~2мин), /healthz, чистый список маршрутов
- app.js: таймаут fetch (AbortController), защита от гонок при переключении вкладок (requestSeq)
- style.css: стили chипа ошибки кэша (multi-line заметки уже поддерживались)
This commit is contained in:
2026-09-09 11:33:43 +03:00
parent 86c2a3097d
commit 2febf9b4ff
3 changed files with 90 additions and 34 deletions

View File

@@ -19,6 +19,8 @@ const PARITY_CLASS = { ч: "badge badge-ch", з: "badge badge-zn" };
let currentParity = null;
let mode = "groups";
let requestSeq = 0;
const FETCH_TIMEOUT_MS = 30000;
function todayName() {
return RUSSIAN_DAYS[new Date().getDay()];
@@ -29,7 +31,16 @@ function enc(value) {
}
async function fetchJSON(url) {
const resp = await fetch(url);
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
let resp;
try {
resp = await fetch(url, { signal: controller.signal });
} catch (err) {
throw new Error(err.name === "AbortError" ? "Время запроса истекло" : err.message);
} finally {
clearTimeout(timer);
}
if (!resp.ok) {
let detail = resp.statusText;
try {
@@ -44,7 +55,7 @@ async function fetchJSON(url) {
async function loadWeek() {
try {
const data = await fetchJSON("/api/week");
currentParity = data.parity;
if (data) currentParity = data.parity;
} catch (_) {}
}
@@ -104,32 +115,37 @@ async function setMode(next) {
await renderView();
}
async function renderView(force = false) {
async function renderView() {
const value = groupSelect.value;
if (!value) return;
localStorage.setItem(MODES[mode].valueKey, value);
const seq = ++requestSeq;
scheduleEl.innerHTML = "";
errorEl.classList.add("hidden");
loadingEl.classList.remove("hidden");
try {
let data, status;
if (mode === "groups") {
const [data, status] = await Promise.all([
fetchJSON(`/api/schedule/${enc(value)}${force ? "?force=1" : ""}`),
[data, status] = await Promise.all([
fetchJSON(`/api/schedule/${enc(value)}`),
loadStatus(),
]);
if (seq !== requestSeq) return;
renderGroupSchedule(data, status);
} else {
const [data, status] = await Promise.all([
[data, status] = await Promise.all([
fetchJSON(`/api/teacher/${enc(value)}`),
loadStatus(),
]);
if (seq !== requestSeq) return;
renderTeacher(data, status);
}
} catch (err) {
if (seq !== requestSeq) return;
errorEl.textContent = "Ошибка: " + err.message;
errorEl.classList.remove("hidden");
} finally {
loadingEl.classList.add("hidden");
if (seq === requestSeq) loadingEl.classList.add("hidden");
}
}
@@ -157,6 +173,11 @@ function statusRowHTML(status) {
const synced = formatDT(status.last_updated);
if (modified) row.appendChild(statusChip("Изменение таблицы", modified));
if (synced) row.appendChild(statusChip("Синхронизация", synced));
if (status.error) {
const chip = statusChip("Ошибка", status.error);
chip.classList.add("error");
row.appendChild(chip);
}
}
if (currentParity) {
const chip = document.createElement("div");
@@ -371,11 +392,14 @@ const FREE_WEEKDAYS = ["понедельник", "вторник", "среда",
async function renderRoomsView(day) {
const chosenDay = FREE_WEEKDAYS.includes(day) ? day : (FREE_WEEKDAYS.includes(todayName()) ? todayName() : "понедельник");
const seq = ++requestSeq;
scheduleEl.innerHTML = "";
errorEl.classList.add("hidden");
loadingEl.classList.remove("hidden");
try {
const status = await loadStatus();
const data = await fetchJSON(`/api/rooms-day?day=${enc(chosenDay)}`);
if (seq !== requestSeq) return;
scheduleEl.appendChild(Object.assign(document.createElement("div"), {
className: "schedule-title",
textContent: "Занятость аудиторий",
@@ -386,14 +410,13 @@ async function renderRoomsView(day) {
const wrap = document.createElement("div");
wrap.className = "rooms-wrap";
scheduleEl.appendChild(wrap);
const data = await fetchJSON(`/api/rooms-day?day=${enc(chosenDay)}`);
wrap.appendChild(roomsDayTable(data));
} catch (err) {
if (seq !== requestSeq) return;
errorEl.textContent = "Ошибка: " + err.message;
errorEl.classList.remove("hidden");
} finally {
loadingEl.classList.add("hidden");
if (seq === requestSeq) loadingEl.classList.add("hidden");
}
}