feat: русский UI, версии в футере, даты и устойчивость загрузки API
Some checks failed
CI / build-and-test (pull_request) Has been cancelled

- API: дата старта всегда YYYY-MM-DD; фронт: parseRaceDate без двойного T00:00:00
- GET /health с version из package.json; Vite define __FRONTEND_VERSION__
- Футер с версиями клиента/сервера (BEM), сетка app-shell на три ряда
- AbortController для карточки старта; ретраи GET при 502–504 и понятные ошибки шлюза
- Русские подписи навигации/страниц, lang=ru, без английских фраз в интерфейсе
This commit is contained in:
Vaka.pro
2026-04-08 00:40:03 +03:00
parent fc995ed07d
commit 42ee36d0a2
22 changed files with 251 additions and 77 deletions

View File

@@ -4,7 +4,7 @@
*/
export interface RaceRow {
id: string;
race_date: string;
race_date: string | Date;
title: string;
distance_km: string;
status: string | null;
@@ -43,11 +43,23 @@ function toISOString(value: Date | string): string {
return value instanceof Date ? value.toISOString() : String(value);
}
/** DATE column may arrive as string or Date; API always exposes YYYY-MM-DD for the calendar day. */
function raceDateToApiValue(value: string | Date): string {
if (typeof value === "string") {
const m = value.match(/^(\d{4}-\d{2}-\d{2})/);
return m ? m[1]! : value;
}
const y = value.getFullYear();
const mo = String(value.getMonth() + 1).padStart(2, "0");
const day = String(value.getDate()).padStart(2, "0");
return `${y}-${mo}-${day}`;
}
/** Convert a DB row to the API DTO (camelCase). */
export function rowToDto(row: RaceRow): RaceDto {
return {
id: row.id,
date: row.race_date,
date: raceDateToApiValue(row.race_date),
title: row.title,
distanceKm: parseFloat(row.distance_km),
status: row.status,

View File

@@ -1,10 +1,11 @@
import { Router, Request, Response } from "express";
import { checkDbConnection } from "../db";
import { getBackendVersion } from "../version";
const router = Router();
router.get("/health", (_req: Request, res: Response) => {
res.json({ status: "ok" });
res.json({ status: "ok", version: getBackendVersion() });
});
router.get("/ready", async (_req: Request, res: Response) => {

14
backend/src/version.ts Normal file
View File

@@ -0,0 +1,14 @@
import fs from "fs";
import path from "path";
let cached: string | null = null;
export function getBackendVersion(): string {
if (cached) {
return cached;
}
const pkgPath = path.join(__dirname, "..", "package.json");
const raw = fs.readFileSync(pkgPath, "utf-8");
cached = (JSON.parse(raw) as { version: string }).version;
return cached;
}

View File

@@ -8,6 +8,8 @@ const app = createApp();
test("GET /health returns ok", async () => {
const res = await request(app).get("/health").expect(200);
assert.equal(res.body.status, "ok");
assert.equal(typeof res.body.version, "string");
assert.ok(res.body.version.length > 0);
});
test("GET /ready succeeds with mock database", async () => {