diff --git a/.gitignore b/.gitignore index f6430cd..8a49b9c 100755 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ credentials.json __pycache__/ .venv/ *.py[cod] +tools/frontend-visual-review/node_modules/ # Local environment files .env diff --git a/CHANGELOG.md b/CHANGELOG.md index 25f7c95..b4d169a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,14 @@ ## Не выпущено +- Frontend `0.2.3`: добавлен публичный поиск по правовой базе с фильтрами, + адаптивной выдачей, просмотром редакций и поиском по тексту документа; + внутренняя лаборатория сохранена, её подсказки стали компактнее и привязаны + к целевым элементам. Интерфейс использует локальные шрифты Noto Sans и Noto + Serif. +- Исправлены переключение на доступный язык редакции, очистка содержимого и + ссылки на официальный источник при смене документа или ошибке загрузки; + подписи фильтров локализуются на кыргызский язык. - Infrastructure: systemd unit Gitea Actions runner теперь использует `ProtectHome=read-only`, сохраняя доступность `/run/user` и существующий writable carve-out для rootless runtime; VPS не изменялся. diff --git a/DESIGN.md b/DESIGN.md index 8e727ab..54ac9e7 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -86,6 +86,15 @@ spacing: генеративного изображения. Название продукта — **Акылдаш**; «УКУК» из ранних концепций не используется. +Изображение определяет целевую композицию, пропорции панелей и расположение +элементов поискового экрана. Эта спецификация определяет семантические токены, +шрифты, доступность и правила поведения. Расхождения между ними фиксируются +до реализации; утверждённые токены не заменяются цветами, взятыми пипеткой +из растрового изображения. Изменения постоянных правил вносятся сюда явно. +Этапы работ, исходные измерения и критерии визуальной приёмки находятся в +[плане точного воспроизведения поискового интерфейса](docs/product/frontend-pixel-perfect-plan.md). +План не является второй дизайн-системой и не означает завершение реализации. + **Key Characteristics:** - Спокойное чтение и компактные, понятные элементы управления. @@ -162,6 +171,34 @@ spacing: Разворачивание документа и поиск внутри него реализуются только вместе с работающим поведением, а не как декоративные кнопки. +### Композиция поискового экрана + +- Первая строка desktop: знак и название, разделитель и подпись продукта + слева; подписанное поле поиска с очисткой и кнопкой «Найти» в центре; + переключатель языка интерфейса справа. Служебные действия авторизованного + пользователя не вытесняют поиск и не меняют эту иерархию. +- Вторая строка: навигация «Поиск» / «Об источниках» и удаляемые активные + фильтры. Сведения о демонстрационном режиме показываются только в нём. +- Ниже расположены три смежные панели: фильтры, результаты, документ. + Иерархия создаётся фонами и непрерывными тонкими разделителями, без + промежутков между самостоятельными карточками панелей. +- Фильтры имеют компактные группы, текстовое действие сброса, выбор органа + и пару полей даты в одной строке при достаточной ширине. Вид элемента + выбора соответствует одиночному или множественному контракту API. +- Заголовок результатов и счётчик находятся слева, сортировка справа. + Высота строк зависит от содержимого; едины отступы и типографика. + Выбранный результат имеет мягкий красный фон, тонкую красную рамку + и текстовый признак открытия. +- Панель документа начинается с контекстной навигации и разворачивания, + затем идут название, компактные метаданные, действия, поиск внутри + документа и единое поле чтения. Название документа имеет отдельную + типографическую роль, отличную от заголовков интерфейсных панелей. +- При коротком содержимом footer расположен у нижней границы окна; + длинное содержимое доступно прокруткой без перекрытия текста и управления. + Бренд находится слева, фактические версии frontend и backend справа. +- Поисковая страница не показывает инструменты оценивания лаборатории. + Общие стили и ресурсы не смешивают композицию этих двух сценариев. + Внутренняя лаборатория сохраняет свой двухпанельный сценарий: фактический порядок результатов и оценивание слева, текст выбранной редакции справа. Она не получает фильтры, аналитику или публичную навигацию исключительно @@ -176,8 +213,9 @@ spacing: горизонтальной прокрутки; широкие таблицы имеют отдельную обозначенную область прокрутки. -Проверочные размеры при реализации: **1440, 1024 и 390 px**. Это размеры -проверки, а не заранее навязанные CSS breakpoints. +Проверочные размеры при реализации: **1280×800, 1440, 1024 и 390 px**. +1280×800 — основной размер сравнения поискового экрана с референсом. +Это размеры проверки, а не заранее навязанные CSS breakpoints. ## Elevation & Depth @@ -204,6 +242,13 @@ spacing: помечается `aria-hidden`, не находится под текстом и не мешает нажатию. На телефоне орнамент сокращается либо убирается. +Поисковый desktop включает небольшой золотистый орнамент в свободной нижней +части панели фильтров. Он не перекрывает длинные списки и не резервирует +место ценой недоступности управления. Знак бренда и орнамент поставляются +как SVG-ресурсы. Favicon использует тот же знак, адаптированный для малых +размеров; проверяется читаемость 16×16 и 32×32. Иконки управления имеют +согласованные толщину линий, оптический размер и выравнивание. + ## Components Это требования к будущей реализации, а не перечень уже обновлённых компонентов. diff --git a/README.md b/README.md index 932be06..c3f7aea 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,8 @@ Telegram-бот — только часть рабочего окружения ## Текущее состояние Сейчас реализованы Telegram-бот-секретарь версии `0.2.2`, backend версии -`0.12.2` и внутренний frontend лаборатории `0.1.2`. Авторизация хранится в +`0.13.0` и frontend `0.2.3` с поисковым экраном и внутренней лабораторией. +Авторизация хранится в SQLite; первый пользователь `admin` создаётся интерактивной CLI-командой. Поиск расширяет аббревиатуры организационно-правовых форм и поднимает действующий Налоговый кодекс для запросов о налоге на прибыль. @@ -18,8 +19,8 @@ SQLite; первый пользователь `admin` создаётся инт | Компонент | Версия | Состояние | |---|---:|---| | Telegram-бот | `0.2.2` | на Synology работает `0.2.1`; обновление после слияния | -| Backend | `0.12.2` | авторизация, поиск с фильтрами и ограниченный staging текущего API Минюста | -| Frontend | `0.1.2` | лаборатория поиска, личный кабинет и управление учетными записями | +| Backend | `0.13.0` | авторизация, поиск с фильтрами, ограниченный staging и статические ресурсы | +| Frontend | `0.2.3` | поиск по правовой базе, просмотр редакций и внутренняя лаборатория | | Сбор и обработка правовых данных | `0.8.0` | добавлены relevance set и оценка выдачи | | RAG и база знаний | — | ещё не созданы | @@ -62,4 +63,4 @@ python3 -m unittest discover -s tools/telegram-bot -v --- -Акылдаш · Telegram-бот v0.2.2 · Backend v0.12.2 · Frontend v0.1.2 +Акылдаш · Telegram-бот v0.2.2 · Backend v0.13.0 · Frontend v0.2.3 diff --git a/backend/README.md b/backend/README.md index 298e191..0aae543 100644 --- a/backend/README.md +++ b/backend/README.md @@ -1,6 +1,6 @@ # Backend Акылдаш -Версия: `0.12.2` +Версия: `0.13.0` Первая backend-область проекта — загрузка правовых документов из официального Open Data API ЦБД Минюста Кыргызской Республики. Код расположен в @@ -296,4 +296,4 @@ PYTHONPATH=backend python3 -m search.query "ЖЧК ачуу тартиби" --la --- -Акылдаш · Backend v0.12.2 · Frontend v0.1.2 +Акылдаш · Backend v0.13.0 · Frontend v0.2.3 diff --git a/backend/ingestion/minjust_current.py b/backend/ingestion/minjust_current.py index 26d4bea..c9db9ad 100644 --- a/backend/ingestion/minjust_current.py +++ b/backend/ingestion/minjust_current.py @@ -23,7 +23,7 @@ from typing import Callable from ingestion.minjust_cbd import atomic_write, connect_manifest, json_bytes API_ROOT = "https://cbd.minjust.gov.kg/api/v1" -APP_VERSION = "0.12.2" +APP_VERSION = "0.13.0" REQUEST_CONTRACT = { "version": 2, "endpoint": "/GetEdition", diff --git a/backend/search/api.py b/backend/search/api.py index 37337ef..46ee5a6 100644 --- a/backend/search/api.py +++ b/backend/search/api.py @@ -26,6 +26,16 @@ LANGUAGES = {"ru", "ky"} CODE = re.compile(r"^[0-9]+$") MAX_PAGE_SIZE = 100 MAX_RESULT_WINDOW = 10_000 +STATIC_ASSETS = { + "/static/favicon.svg": ("favicon.svg", "image/svg+xml"), + "/static/favicon-32.png": ("favicon-32.png", "image/png"), + "/static/fonts/noto-sans-cyrillic-ext.woff2": ("fonts/noto-sans-cyrillic-ext.woff2", "font/woff2"), + "/static/fonts/noto-sans-cyrillic.woff2": ("fonts/noto-sans-cyrillic.woff2", "font/woff2"), + "/static/fonts/noto-sans-latin.woff2": ("fonts/noto-sans-latin.woff2", "font/woff2"), + "/static/fonts/noto-serif-cyrillic-ext.woff2": ("fonts/noto-serif-cyrillic-ext.woff2", "font/woff2"), + "/static/fonts/noto-serif-cyrillic.woff2": ("fonts/noto-serif-cyrillic.woff2", "font/woff2"), + "/static/fonts/noto-serif-latin.woff2": ("fonts/noto-serif-latin.woff2", "font/woff2"), +} class ApiError(Exception): @@ -303,6 +313,17 @@ class Api: except (OSError, UnicodeError) as error: raise ApiError(500, "review interface is unavailable") from error + @staticmethod + def static_asset(route: str) -> tuple[str, bytes]: + asset = STATIC_ASSETS.get(route) + if asset is None: + raise ApiError(404, "static asset not found") + path = Path(__file__).with_name("static") / asset[0] + try: + return asset[1], path.read_bytes() + except OSError as error: + raise ApiError(500, "static asset is unavailable") from error + def handle(self, method: str, path: str, body: dict | None = None, user: dict | None = None) -> tuple[int, dict]: parsed = urllib.parse.urlsplit(path) query = urllib.parse.parse_qs(parsed.query, keep_blank_values=True) @@ -386,6 +407,18 @@ def handler(api: Api): self.end_headers() self.wfile.write(body) return + if method == "GET" and route in STATIC_ASSETS: + content_type, body = api.static_asset(route) + self.send_response(200) + self.send_header("Content-Type", content_type) + self.send_header("Cache-Control", "public, max-age=604800, immutable") + self.send_header("X-Content-Type-Options", "nosniff") + self.send_header("X-Frame-Options", "DENY") + self.send_header("Referrer-Policy", "no-referrer") + self.send_header("Content-Length", str(len(body))) + self.end_headers() + self.wfile.write(body) + return if not (method == "POST" and route in {"/auth/login", "/auth/logout"}): if not user: raise ApiError(401, "authentication required") diff --git a/backend/search/minjust_opensearch.py b/backend/search/minjust_opensearch.py index 0feda71..10b7049 100644 --- a/backend/search/minjust_opensearch.py +++ b/backend/search/minjust_opensearch.py @@ -17,7 +17,7 @@ from typing import Iterator from search.catalog import authority_codes, legal_force_code, source_code -APP_VERSION = "0.12.2" +APP_VERSION = "0.13.0" LANGUAGES = {"ru", "ky"} DEFAULT_MAPPING = Path(__file__).with_name("minjust-fragments-index.json") diff --git a/backend/search/review.html b/backend/search/review.html index d5a42f0..ba7de2d 100644 --- a/backend/search/review.html +++ b/backend/search/review.html @@ -3,8 +3,17 @@
+ + +Сопоставляйте запрос с текстом документов и сохраняйте оценку выдачи.