test: centralize media role labels

This commit is contained in:
ik
2026-09-21 18:06:09 +07:00
parent d32796277c
commit 4f847db38a
4 changed files with 25 additions and 4 deletions
+2 -2
View File
@@ -1,8 +1,8 @@
--- ---
import SourceBadge from "./SourceBadge.astro"; import SourceBadge from "./SourceBadge.astro";
import type { MediaAsset } from "../lib/api"; import type { MediaAsset } from "../lib/api";
import { mediaRoleLabel } from "../lib/media";
const { asset, compact = false, sourceLink = false } = Astro.props as { asset: MediaAsset; compact?: boolean; sourceLink?: boolean }; const { asset, compact = false, sourceLink = false } = Astro.props as { asset: MediaAsset; compact?: boolean; sourceLink?: boolean };
const roleLabels: Record<string, string> = { waterbody_cover: "Заставка водоёма", waterbody_map: "Карта водоёма", waterbody_depth_map: "Карта глубин", waterbody_screenshot: "Скриншот водоёма", tackle_card: "Карточка снасти", tackle_detail: "Деталь снасти", rig_diagram: "Схема монтажа", tackle_screenshot: "Скриншот снасти" };
--- ---
<figure class:list={["entity-media", { "entity-media--compact": compact }]}> <figure class:list={["entity-media", { "entity-media--compact": compact }]}>
<span class="entity-media__frame"><picture> <span class="entity-media__frame"><picture>
@@ -12,5 +12,5 @@ const roleLabels: Record<string, string> = { waterbody_cover: "Заставка
})} })}
<img src={asset.image_url} alt={asset.label ?? "Иллюстрация RF4"} width={asset.width} height={asset.height} loading="lazy" decoding="async" /> <img src={asset.image_url} alt={asset.label ?? "Иллюстрация RF4"} width={asset.width} height={asset.height} loading="lazy" decoding="async" />
</picture></span> </picture></span>
<figcaption><SourceBadge source={asset.source_system} href={sourceLink ? asset.source_url : undefined} /><span>{asset.label ?? "Справочный материал"}</span>{asset.media_role && <small data-media-role={asset.media_role}>{roleLabels[asset.media_role] ?? asset.media_role}</small>}</figcaption> <figcaption><SourceBadge source={asset.source_system} href={sourceLink ? asset.source_url : undefined} /><span>{asset.label ?? "Справочный материал"}</span>{asset.media_role && <small data-media-role={asset.media_role}>{mediaRoleLabel(asset.media_role)}</small>}</figcaption>
</figure> </figure>
+13
View File
@@ -1,5 +1,18 @@
import type { MediaAsset, MediaRole } from "./api"; import type { MediaAsset, MediaRole } from "./api";
const mediaRoleLabels: Record<MediaRole, string> = {
waterbody_cover: "Заставка водоёма",
waterbody_map: "Карта водоёма",
waterbody_depth_map: "Карта глубин",
waterbody_screenshot: "Скриншот водоёма",
tackle_card: "Карточка снасти",
tackle_detail: "Деталь снасти",
rig_diagram: "Схема монтажа",
tackle_screenshot: "Скриншот снасти",
};
export const mediaRoleLabel = (role: string): string => mediaRoleLabels[role as MediaRole] ?? role;
const tokens = (value: string) => value const tokens = (value: string) => value
.toLocaleLowerCase("ru") .toLocaleLowerCase("ru")
.replaceAll("ё", "е") .replaceAll("ё", "е")
+9 -1
View File
@@ -7,7 +7,7 @@ import { tackleVisualKind, tackleVisualTone } from "../../src/lib/tackle-visuals
import { waterbodyVisual } from "../../src/lib/waterbody-visuals.ts"; import { waterbodyVisual } from "../../src/lib/waterbody-visuals.ts";
import { pageHref, pageWindow } from "../../src/lib/pagination.ts"; import { pageHref, pageWindow } from "../../src/lib/pagination.ts";
import { adminEndsSession, adminErrorMessage } from "../../src/lib/admin-errors.ts"; import { adminEndsSession, adminErrorMessage } from "../../src/lib/admin-errors.ts";
import { findMediaByLabel } from "../../src/lib/media.ts"; import { findMediaByLabel, mediaRoleLabel } from "../../src/lib/media.ts";
test("activity levels share one complete 0-100 scale", () => { test("activity levels share one complete 0-100 scale", () => {
assert.deepEqual( assert.deepEqual(
@@ -79,6 +79,14 @@ test("waterbody media lookup only accepts reviewed roles when constrained", () =
assert.equal(findMediaByLabel([...assets], "Куори", ["waterbody_cover"]), undefined); assert.equal(findMediaByLabel([...assets], "Куори", ["waterbody_cover"]), undefined);
}); });
test("reviewed media roles keep human-readable labels with a safe fallback", () => {
assert.deepEqual(
["waterbody_cover", "waterbody_map", "waterbody_depth_map", "waterbody_screenshot", "tackle_card", "tackle_detail", "rig_diagram", "tackle_screenshot"].map(mediaRoleLabel),
["Заставка водоёма", "Карта водоёма", "Карта глубин", "Скриншот водоёма", "Карточка снасти", "Деталь снасти", "Схема монтажа", "Скриншот снасти"],
);
assert.equal(mediaRoleLabel("future_role"), "future_role");
});
test("45 records are reachable across three stable pages", () => { test("45 records are reachable across three stable pages", () => {
const pages = [pageWindow(45, 20, 0), pageWindow(45, 20, 20), pageWindow(45, 20, 40)]; 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.start, page.end]), [[1, 20], [21, 40], [41, 45]]);
+1 -1
View File
@@ -73,7 +73,7 @@
- [ ] **G05 · Связи с уловами и источниками.** Добавлены `catch_tackle_component` и offline `gear_components`: можно сохранять несколько unresolved/canonical компонентов с ролью, порядком, исходным значением, source identity и `raw_payload`; legacy `bait_id` не меняется. Parser сохраняет порядок оборудования из detail и разделяет bait/rig в catch-полях. Запись компонентов подключена к community import, официальному импорту и пользовательской форме; повторная обработка идемпотентна. Canonical-привязка и безопасный backfill остаются только после подтверждённого crosswalk. - [ ] **G05 · Связи с уловами и источниками.** Добавлены `catch_tackle_component` и offline `gear_components`: можно сохранять несколько unresolved/canonical компонентов с ролью, порядком, исходным значением, source identity и `raw_payload`; legacy `bait_id` не меняется. Parser сохраняет порядок оборудования из detail и разделяет bait/rig в catch-полях. Запись компонентов подключена к community import, официальному импорту и пользовательской форме; повторная обработка идемпотентна. Canonical-привязка и безопасный backfill остаются только после подтверждённого crosswalk.
- [ ] **G06 · API и публичный каталог.** Добавлены пагинированный `/api/v1/tackle/items` с фильтрами по категории, бренду, семейству и уровню, detail endpoints для предмета и монтажа, а также ordered `tackle_components` в ответе уловов точки. Ответы показывают только канонические характеристики, provenance, timestamp проверки и `missing_fields`; рейтинг эффективности не добавляется. Публичный каталог теперь имеет detail-route `/tackle/items/:id` с 404/unavailable-различением, паспортом данных и явным списком недостающих полей; CatchList показывает ordered-компоненты, ведёт в canonical detail только при наличии `tackle_item_id`, а unresolved raw value оставляет текстом. - [ ] **G06 · API и публичный каталог.** Добавлены пагинированный `/api/v1/tackle/items` с фильтрами по категории, бренду, семейству и уровню, detail endpoints для предмета и монтажа, а также ordered `tackle_components` в ответе уловов точки. Ответы показывают только канонические характеристики, provenance, timestamp проверки и `missing_fields`; рейтинг эффективности не добавляется. Публичный каталог теперь имеет detail-route `/tackle/items/:id` с 404/unavailable-различением, паспортом данных и явным списком недостающих полей; CatchList показывает ordered-компоненты, ведёт в canonical detail только при наличии `tackle_item_id`, а unresolved raw value оставляет текстом.
- [ ] **G07 · Аналитика сочетаний и рекомендации.** Добавлен `/api/v1/analytics/tackle`: approved-наблюдения группируются по роли и исходному компоненту с фильтрами водоёма, рыбы, метода и окна; дубликаты одного улова не увеличивают счётчик, а минимум наблюдений и независимых игроков отделяет факт использования от рекомендации. Публичный `/tackle/analytics` показывает фильтры, выборку, независимых игроков и раздельные `recommendation`/`insufficient_data` states без рейтинга эффективности. Добавлен объяснимый `freshness_score` с half-life `12.5` часа и контрактом `0..100`: он не меняет raw-счётчики и пороги, но стабильно ставит более свежую равную выборку выше старой и виден на карточке. Остаётся дальнейшая визуальная приёмка. - [ ] **G07 · Аналитика сочетаний и рекомендации.** Добавлен `/api/v1/analytics/tackle`: approved-наблюдения группируются по роли и исходному компоненту с фильтрами водоёма, рыбы, метода и окна; дубликаты одного улова не увеличивают счётчик, а минимум наблюдений и независимых игроков отделяет факт использования от рекомендации. Публичный `/tackle/analytics` показывает фильтры, выборку, независимых игроков и раздельные `recommendation`/`insufficient_data` states без рейтинга эффективности. Добавлен объяснимый `freshness_score` с half-life `12.5` часа и контрактом `0..100`: он не меняет raw-счётчики и пороги, но стабильно ставит более свежую равную выборку выше старой и виден на карточке. Остаётся дальнейшая визуальная приёмка.
- [ ] **G08 · Медиа и качество.** Добавлены отдельные reviewed-роли `tackle_card`, `tackle_detail`, `rig_diagram`, `tackle_screenshot` для `tackle`, а также CLI-параметр `--media-role`; offline audit отклоняет неизвестную роль и несовпадение роли с entity type. Существующие dimensions, MIME, SHA-256, прозрачность, aspect ratio, provenance и атомарное продвижение сохраняются. Остаётся провести реальный contact-sheet review для будущих tackle-кандидатов без автоматической публикации. - [ ] **G08 · Медиа и качество.** Добавлены отдельные reviewed-роли `tackle_card`, `tackle_detail`, `rig_diagram`, `tackle_screenshot` для `tackle`, а также CLI-параметр `--media-role`; offline audit отклоняет неизвестную роль и несовпадение роли с entity type. Существующие dimensions, MIME, SHA-256, прозрачность, aspect ratio, provenance и атомарное продвижение сохраняются. UI использует единый словарь человекочитаемых role labels с безопасным fallback для будущих ролей, покрытый unit-регрессией. Остаётся провести реальный contact-sheet review для будущих tackle-кандидатов без автоматической публикации.
- [ ] **G09 · Приёмка и эксплуатация.** Добавлены fixture/regression tests для crosswalk, media roles, идемпотентных компонентов, пустых результатов и многокомпонентных наблюдений; каталог `/tackle` включён в visual-matrix, narrow smoke и accessibility routes. Offline catalog/media audits и сохранение старых данных при сбое импорта проходят. TEMP-only query-plan gate для фильтра каталога и группировки сочетаний использует индексы и укладывается в 250 мс. Chromium подтвердил empty-state публичного каталога и отсутствие overflow на 320 px; обычный E2E пропускает bootstrap без явных переменных, отдельный bootstrap с токеном проходит. Остаются HTTP/browser acceptance для неоднозначных и многокомпонентных комплектов, ручной visual review и проверяемый счётчик по категориям либо явный `unknown`. Сетевые тесты не выполнять; импорт оставить opt-in, последовательным и под общим cooldown/backoff. - [ ] **G09 · Приёмка и эксплуатация.** Добавлены fixture/regression tests для crosswalk, media roles, идемпотентных компонентов, пустых результатов и многокомпонентных наблюдений; каталог `/tackle` включён в visual-matrix, narrow smoke и accessibility routes. Offline catalog/media audits и сохранение старых данных при сбое импорта проходят. TEMP-only query-plan gate для фильтра каталога и группировки сочетаний использует индексы и укладывается в 250 мс. Chromium подтвердил empty-state публичного каталога и отсутствие overflow на 320 px; обычный E2E пропускает bootstrap без явных переменных, отдельный bootstrap с токеном проходит. Остаются HTTP/browser acceptance для неоднозначных и многокомпонентных комплектов, ручной visual review и проверяемый счётчик по категориям либо явный `unknown`. Сетевые тесты не выполнять; импорт оставить opt-in, последовательным и под общим cooldown/backoff.
### Тёмная тема ### Тёмная тема