fix: friendly 502 handling, 2 uvicorn workers, file bundle cache (0.20.18)

This commit is contained in:
PTah
2026-06-20 20:01:46 +10:00
parent 4d9a9e7e2b
commit f3a8952947
8 changed files with 63 additions and 40 deletions
+20 -5
View File
@@ -62,20 +62,20 @@ export async function apiFetch<T>(path: string, init: RequestInit = {}): Promise
clearToken();
redirectToErrorPage(401);
}
throw new ApiError(parseApiError(text) || "Unauthorized", 401);
throw new ApiError(parseApiError(text, 401) || "Unauthorized", 401);
}
if (res.status === 403) {
if (hadToken) {
redirectToErrorPage(403);
}
throw new ApiError(parseApiError(text) || "Forbidden", 403);
throw new ApiError(parseApiError(text, 403) || "Forbidden", 403);
}
if (res.status === 429) {
redirectToErrorPage(429);
throw new ApiError(parseApiError(text) || "Too many requests", 429);
throw new ApiError(parseApiError(text, 429) || "Too many requests", 429);
}
if (!res.ok) {
throw new ApiError(parseApiError(text) || res.statusText, res.status);
throw new ApiError(parseApiError(text, res.status) || res.statusText, res.status);
}
if (!text) {
return undefined as T;
@@ -99,8 +99,23 @@ export function fetchMe(): Promise<MeResponse> {
return apiFetch<MeResponse>("/api/v1/auth/me");
}
export function parseApiError(text: string): string {
export function parseApiError(text: string, status?: number): string {
if (status === 502 || status === 503 || status === 504) {
if (/<title>\s*502 Bad Gateway/i.test(text) || /502 Bad Gateway/i.test(text)) {
return "Сервер SAC временно недоступен (перезапуск или обновление). Подождите 5–10 секунд и обновите страницу.";
}
if (/<title>\s*503 Service Unavailable/i.test(text) || /503 Service Unavailable/i.test(text)) {
return "SAC временно перегружен. Повторите запрос через несколько секунд.";
}
if (/<title>\s*504 Gateway Time-out/i.test(text) || /504 Gateway Time-out/i.test(text)) {
return "Превышено время ожидания ответа SAC. Долгая операция могла ещё выполняться на сервере.";
}
return "Временная ошибка шлюза SAC. Обновите страницу.";
}
if (!text) return "";
if (text.trimStart().startsWith("<")) {
return status ? `Ошибка HTTP ${status}` : "Ошибка сервера";
}
try {
const data = JSON.parse(text) as { detail?: string | Array<{ msg?: string }> };
if (typeof data.detail === "string") return data.detail;
+1 -1
View File
@@ -1,4 +1,4 @@
/** Fallback до загрузки /health; при релизе держите в sync с backend/app/version.py */
export const APP_NAME = "Security Alert Center";
export const APP_VERSION = "0.20.17";
export const APP_VERSION = "0.20.18";
export const APP_VERSION_LABEL = `${APP_NAME} v.${APP_VERSION}`;
+7 -1
View File
@@ -101,6 +101,7 @@ import { useRoute, useRouter } from "vue-router";
import { useSacLiveEventStream } from "../composables/useSacLiveEventStream";
import {
apiFetch,
ApiError,
fetchHost,
fetchRecentEvents,
type HostDetail,
@@ -304,7 +305,7 @@ const sortedItems = computed(() => {
return items;
});
async function loadHosts() {
async function loadHosts(retry = 0) {
loading.value = true;
error.value = "";
try {
@@ -314,6 +315,11 @@ async function loadHosts() {
if (agentStatusFilter.value) params.set("agent_status", agentStatusFilter.value);
data.value = await apiFetch<HostListResponse>(`/api/v1/hosts?${params}`);
} catch (e) {
const status = e instanceof ApiError ? e.status : 0;
if (retry < 2 && (status === 502 || status === 503)) {
await new Promise((resolve) => window.setTimeout(resolve, 2500));
return loadHosts(retry + 1);
}
error.value = e instanceof Error ? e.message : "Ошибка";
} finally {
loading.value = false;