diff --git a/apps/web/src/lib/admin-errors.ts b/apps/web/src/lib/admin-errors.ts new file mode 100644 index 0000000..d4efeeb --- /dev/null +++ b/apps/web/src/lib/admin-errors.ts @@ -0,0 +1,11 @@ +export function adminErrorMessage(status: number, fallback: string): string { + if (status === 401) return "Неверный или истёкший административный токен."; + if (status === 409) return "Операция конфликтует с изменением в другой вкладке."; + if (status === 429) return "Слишком много попыток. Повторите позже."; + if (status >= 500) return "Сервис временно недоступен. Проверьте состояние и повторите позже."; + return fallback; +} + +export function adminEndsSession(status: number): boolean { + return status === 401 || status === 429; +} diff --git a/apps/web/src/pages/admin/external-sources.astro b/apps/web/src/pages/admin/external-sources.astro index 3e2c1c9..1b5e98b 100644 --- a/apps/web/src/pages/admin/external-sources.astro +++ b/apps/web/src/pages/admin/external-sources.astro @@ -24,6 +24,7 @@ const apiUrl = import.meta.env.PUBLIC_API_URL || "http://localhost:8000";

S подсказать · M сопоставить · P опубликовать карточку с фокусом

diff --git a/apps/web/tests/unit/presentation.test.ts b/apps/web/tests/unit/presentation.test.ts index 5848fc1..a923cab 100644 --- a/apps/web/tests/unit/presentation.test.ts +++ b/apps/web/tests/unit/presentation.test.ts @@ -5,6 +5,8 @@ import { activityLevel, plural } from "../../src/lib/presentation.ts"; import { fishVisualFamily } from "../../src/lib/fish-visuals.ts"; import { tackleVisualKind, tackleVisualTone } from "../../src/lib/tackle-visuals.ts"; import { waterbodyVisual } from "../../src/lib/waterbody-visuals.ts"; +import { pageHref, pageWindow } from "../../src/lib/pagination.ts"; +import { adminEndsSession, adminErrorMessage } from "../../src/lib/admin-errors.ts"; test("activity levels share one complete 0-100 scale", () => { assert.deepEqual( @@ -46,3 +48,31 @@ test("waterbody fingerprints are stable, bounded and entity-specific", () => { assert.ok(kuori.markerY >= 18 && kuori.markerY <= 40); assert.match(kuori.code, /^[0-9A-Z]{2}$/); }); + +test("45 records are reachable across three stable pages", () => { + const pages = [pageWindow(45, 20, 0), pageWindow(45, 20, 20), pageWindow(45, 20, 40)]; + assert.deepEqual(pages.map(page => [page.start, page.end]), [[1, 20], [21, 40], [41, 45]]); + assert.deepEqual(pages.map(page => [page.previousOffset, page.nextOffset]), [[null, 20], [0, 40], [20, null]]); + assert.deepEqual(pages.map(page => page.page), [1, 2, 3]); + const reached = pages.flatMap(page => Array.from({ length: page.end - page.start + 1 }, (_, index) => page.start + index)); + assert.deepEqual(reached, Array.from({ length: 45 }, (_, index) => index + 1)); +}); + +test("pagination links preserve filters and replace offset", () => { + const params = new URLSearchParams("fish=pike&waterbody=kuori&offset=20&offset=999"); + const next = pageHref("/records", params, 40); + const first = pageHref("/records", params, 0, "#results"); + assert.equal(next, "/records?fish=pike&waterbody=kuori&offset=40"); + assert.equal(first, "/records?fish=pike&waterbody=kuori#results"); +}); + +test("admin errors keep auth and operational responses consistent", () => { + assert.equal(adminErrorMessage(401, "fallback"), "Неверный или истёкший административный токен."); + assert.equal(adminErrorMessage(409, "fallback"), "Операция конфликтует с изменением в другой вкладке."); + assert.equal(adminErrorMessage(429, "fallback"), "Слишком много попыток. Повторите позже."); + assert.equal(adminErrorMessage(503, "fallback"), "Сервис временно недоступен. Проверьте состояние и повторите позже."); + assert.equal(adminErrorMessage(422, "fallback"), "fallback"); + assert.equal(adminEndsSession(401), true); + assert.equal(adminEndsSession(429), true); + assert.equal(adminEndsSession(409), false); +});