Compare commits
2 Commits
fix/api-pr
...
feat/foote
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
83bc603b95 | ||
| f8b4ce7111 |
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "calendar-run-backend",
|
"name": "calendar-run-backend",
|
||||||
"version": "1.0.1",
|
"version": "1.1.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "tsc",
|
"build": "tsc",
|
||||||
|
|||||||
@@ -7,7 +7,9 @@ import racesRouter from "./routes/races";
|
|||||||
export function createApp(): express.Express {
|
export function createApp(): express.Express {
|
||||||
const app = express();
|
const app = express();
|
||||||
|
|
||||||
app.use(cors({ origin: config.corsOrigin, methods: ["GET", "POST", "PATCH", "DELETE"] }));
|
app.use(
|
||||||
|
cors({ origin: config.corsOrigin, methods: ["GET", "POST", "PATCH", "DELETE", "OPTIONS"] }),
|
||||||
|
);
|
||||||
app.use(express.json());
|
app.use(express.json());
|
||||||
|
|
||||||
app.use(healthRouter);
|
app.use(healthRouter);
|
||||||
|
|||||||
@@ -8,6 +8,11 @@ router.get("/health", (_req: Request, res: Response) => {
|
|||||||
res.json({ status: "ok", version: getBackendVersion() });
|
res.json({ status: "ok", version: getBackendVersion() });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/** Версия для UI; путь без «health», чтобы реже резался фильтрами/прокси. */
|
||||||
|
router.get("/meta", (_req: Request, res: Response) => {
|
||||||
|
res.json({ version: getBackendVersion() });
|
||||||
|
});
|
||||||
|
|
||||||
router.get("/ready", async (_req: Request, res: Response) => {
|
router.get("/ready", async (_req: Request, res: Response) => {
|
||||||
const dbOk = await checkDbConnection();
|
const dbOk = await checkDbConnection();
|
||||||
if (dbOk) {
|
if (dbOk) {
|
||||||
|
|||||||
@@ -17,6 +17,18 @@ test("GET /api/health returns ok (prefix without proxy strip)", async () => {
|
|||||||
assert.equal(res.body.status, "ok");
|
assert.equal(res.body.status, "ok");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("GET /meta returns version for UI footer", async () => {
|
||||||
|
const res = await request(app).get("/meta").expect(200);
|
||||||
|
assert.equal(typeof res.body.version, "string");
|
||||||
|
assert.ok(res.body.version.length > 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("GET /api/meta mirrors GET /meta", async () => {
|
||||||
|
const res = await request(app).get("/api/meta").expect(200);
|
||||||
|
assert.equal(typeof res.body.version, "string");
|
||||||
|
assert.ok(res.body.version.length > 0);
|
||||||
|
});
|
||||||
|
|
||||||
test("GET /ready succeeds with mock database", async () => {
|
test("GET /ready succeeds with mock database", async () => {
|
||||||
const res = await request(app).get("/ready").expect(200);
|
const res = await request(app).get("/ready").expect(200);
|
||||||
assert.equal(res.body.status, "ready");
|
assert.equal(res.body.status, "ready");
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "calendar-run-frontend",
|
"name": "calendar-run-frontend",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.1.1",
|
"version": "0.2.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
|
|||||||
@@ -5,6 +5,15 @@ export type HealthResponse = {
|
|||||||
version: string;
|
version: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type BackendMetaResponse = {
|
||||||
|
version: string;
|
||||||
|
};
|
||||||
|
|
||||||
export async function getHealth(init?: RequestInit): Promise<HealthResponse> {
|
export async function getHealth(init?: RequestInit): Promise<HealthResponse> {
|
||||||
return requestJson<HealthResponse>("/health", init);
|
return requestJson<HealthResponse>("/health", init);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Версия бэкенда для футера (отдельный путь от /health — меньше ложных блокировок). */
|
||||||
|
export async function getBackendMeta(init?: RequestInit): Promise<BackendMetaResponse> {
|
||||||
|
return requestJson<BackendMetaResponse>("/meta", init);
|
||||||
|
}
|
||||||
|
|||||||
@@ -8,16 +8,30 @@ function buildUrl(path: string): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function parseResponseBody(response: Response): Promise<unknown> {
|
async function parseResponseBody(response: Response): Promise<unknown> {
|
||||||
const contentType = response.headers.get("content-type") ?? "";
|
const text = await response.text();
|
||||||
if (!contentType.includes("application/json")) {
|
if (!text.trim()) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const contentType = response.headers.get("content-type") ?? "";
|
||||||
|
if (contentType.includes("application/json")) {
|
||||||
try {
|
try {
|
||||||
return await response.json();
|
return JSON.parse(text) as unknown;
|
||||||
} catch {
|
} catch {
|
||||||
return null;
|
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]);
|
const GATEWAY_RETRY_STATUSES = new Set([502, 503, 504]);
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
export type { CreateRacePayload, Race, RacesQuery, RaceStatus, UpdateRacePayload } from "./types";
|
export type { CreateRacePayload, Race, RacesQuery, RaceStatus, UpdateRacePayload } from "./types";
|
||||||
export { ApiError, getApiErrorMessage } from "./errors";
|
export { ApiError, getApiErrorMessage } from "./errors";
|
||||||
export type { HealthResponse } from "./health";
|
export type { BackendMetaResponse, HealthResponse } from "./health";
|
||||||
export { getHealth } from "./health";
|
export { getBackendMeta, getHealth } from "./health";
|
||||||
export { getRaceById, getRaces, createRace, updateRace, deleteRace } from "./races";
|
export { getRaceById, getRaces, createRace, updateRace, deleteRace } from "./races";
|
||||||
|
|||||||
@@ -1,22 +1,29 @@
|
|||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { getHealth } from "../../api";
|
import { getBackendMeta } from "../../api";
|
||||||
import { FRONTEND_VERSION } from "../../frontendVersion";
|
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 {
|
export function AppShellFooter(): JSX.Element {
|
||||||
const [backendVersion, setBackendVersion] = useState<string | null>(null);
|
const [backendVersion, setBackendVersion] = useState<string | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const ac = new AbortController();
|
const ac = new AbortController();
|
||||||
void getHealth({ signal: ac.signal })
|
void getBackendMeta({ signal: ac.signal })
|
||||||
.then((h) => {
|
.then((meta) => {
|
||||||
if (ac.signal.aborted) {
|
if (ac.signal.aborted) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const v = h.version;
|
const v = meta.version;
|
||||||
setBackendVersion(typeof v === "string" && v.length > 0 ? v : "не указана");
|
setBackendVersion(typeof v === "string" && v.length > 0 ? v : "не указана");
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch((err) => {
|
||||||
if (ac.signal.aborted) {
|
if (ac.signal.aborted || isAbortError(err)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setBackendVersion("недоступна");
|
setBackendVersion("недоступна");
|
||||||
|
|||||||
Reference in New Issue
Block a user