feat: /meta для версии в футере и устойчивый разбор JSON
Some checks failed
CI / build-and-test (pull_request) Has been cancelled

This commit is contained in:
Anton
2026-04-08 10:32:52 +03:00
parent f8b4ce7111
commit 83bc603b95
9 changed files with 66 additions and 17 deletions

View File

@@ -1,7 +1,7 @@
{
"name": "calendar-run-frontend",
"private": true,
"version": "0.1.1",
"version": "0.2.0",
"type": "module",
"scripts": {
"dev": "vite",

View File

@@ -5,6 +5,15 @@ export type HealthResponse = {
version: string;
};
export type BackendMetaResponse = {
version: string;
};
export async function getHealth(init?: RequestInit): Promise<HealthResponse> {
return requestJson<HealthResponse>("/health", init);
}
/** Версия бэкенда для футера (отдельный путь от /health — меньше ложных блокировок). */
export async function getBackendMeta(init?: RequestInit): Promise<BackendMetaResponse> {
return requestJson<BackendMetaResponse>("/meta", init);
}

View File

@@ -8,16 +8,30 @@ function buildUrl(path: string): string {
}
async function parseResponseBody(response: Response): Promise<unknown> {
const contentType = response.headers.get("content-type") ?? "";
if (!contentType.includes("application/json")) {
const text = await response.text();
if (!text.trim()) {
return null;
}
try {
return await response.json();
} catch {
return null;
const contentType = response.headers.get("content-type") ?? "";
if (contentType.includes("application/json")) {
try {
return JSON.parse(text) as unknown;
} catch {
return null;
}
}
const trimmed = text.trim();
if (trimmed.startsWith("{") || trimmed.startsWith("[")) {
try {
return JSON.parse(text) as unknown;
} catch {
return null;
}
}
return null;
}
const GATEWAY_RETRY_STATUSES = new Set([502, 503, 504]);

View File

@@ -1,5 +1,5 @@
export type { CreateRacePayload, Race, RacesQuery, RaceStatus, UpdateRacePayload } from "./types";
export { ApiError, getApiErrorMessage } from "./errors";
export type { HealthResponse } from "./health";
export { getHealth } from "./health";
export type { BackendMetaResponse, HealthResponse } from "./health";
export { getBackendMeta, getHealth } from "./health";
export { getRaceById, getRaces, createRace, updateRace, deleteRace } from "./races";

View File

@@ -1,22 +1,29 @@
import { useEffect, useState } from "react";
import { getHealth } from "../../api";
import { getBackendMeta } from "../../api";
import { FRONTEND_VERSION } from "../../frontendVersion";
function isAbortError(error: unknown): boolean {
return (
(error instanceof DOMException && error.name === "AbortError") ||
(error instanceof Error && error.name === "AbortError")
);
}
export function AppShellFooter(): JSX.Element {
const [backendVersion, setBackendVersion] = useState<string | null>(null);
useEffect(() => {
const ac = new AbortController();
void getHealth({ signal: ac.signal })
.then((h) => {
void getBackendMeta({ signal: ac.signal })
.then((meta) => {
if (ac.signal.aborted) {
return;
}
const v = h.version;
const v = meta.version;
setBackendVersion(typeof v === "string" && v.length > 0 ? v : "не указана");
})
.catch(() => {
if (ac.signal.aborted) {
.catch((err) => {
if (ac.signal.aborted || isAbortError(err)) {
return;
}
setBackendVersion("недоступна");