diff --git a/.gitea/workflows/ci.yaml b/.gitea/workflows/ci.yaml new file mode 100644 index 0000000..e15185e --- /dev/null +++ b/.gitea/workflows/ci.yaml @@ -0,0 +1,42 @@ +name: CI + +on: + pull_request: + push: + branches: [main] + +jobs: + tests-and-images: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: "3.12" + - name: Run backend tests + run: PYTHONPATH=backend python3 -m unittest discover -s backend -p 'test_*.py' -v + - name: Run Telegram bot tests + run: python3 -m unittest discover -s tools/telegram-bot -p 'test_*.py' -v + - name: Run infrastructure backup tests + run: python3 -m unittest discover -s infra/backup -p 'test_*.py' -v + - name: Check recovery host guard + run: bash infra/bootstrap/test-host-guard.sh + - name: Check verifier helper argument guard + run: bash infra/bootstrap/test-verify-helper.sh + - name: Check infrastructure shell syntax + run: | + bash -n infra/bootstrap/bootstrap-ubuntu-24.04.sh infra/bootstrap/host-guard.sh \ + infra/bootstrap/verify.sh infra/bootstrap/verify-helper.sh infra/deploy/install-helper.sh \ + infra/bootstrap/test-verify-helper.sh + - name: Run rollback helper tests + run: python3 -m unittest discover -s infra/deploy -p 'test_*.py' -v + - name: Validate production Compose and Caddyfile + run: | + secret_file=$(mktemp) + trap 'rm -f "$secret_file"' EXIT + REVIEW_SECRET_FILE="$secret_file" docker compose --env-file deploy/production/.env.example -f deploy/production/compose.yaml config --quiet + docker run --rm --entrypoint caddy -v "$PWD/deploy/production/Caddyfile:/etc/caddy/Caddyfile:ro" caddy:2.11.6-alpine@sha256:881bbc60f9986d5ab8e7cfd6cf7e4ef3c9c0439fef2429d035d065577882f028 validate --config /etc/caddy/Caddyfile --adapter caddyfile + - name: Build production images + run: | + docker build -f deploy/production/Dockerfile.backend -t akyldash-backend:ci . + docker build -f deploy/production/Dockerfile.opensearch -t akyldash-opensearch:ci . diff --git a/.impeccable/config.json b/.impeccable/config.json new file mode 100644 index 0000000..496c0ff --- /dev/null +++ b/.impeccable/config.json @@ -0,0 +1,35 @@ +{ + "detector": { + "ignoreRules": [], + "ignoreFiles": [], + "ignoreValues": [ + { + "rule": "cream-palette", + "value": "*", + "files": [ + "backend/search/review.html" + ], + "createdAt": "2026-10-11T15:40:02.635Z", + "reason": "Agent: DESIGN.md explicitly approves the #F7F2E8 felt-inspired bg token for this portal; current page background matches that user-approved palette" + }, + { + "rule": "cramped-padding", + "value": "*", + "files": [ + "backend/search/review.html" + ], + "createdAt": "2026-10-11T15:41:28.434Z", + "reason": "Agent: current page uses 12–24px padding in bordered containers; the prior stop finding has no element detail and does not reproduce in the current detector" + }, + { + "rule": "design-system-font", + "value": "sans", + "files": [ + "backend/search/review.html" + ], + "createdAt": "2026-10-11T15:41:33.020Z", + "reason": "Agent: DESIGN.md specifies Noto Sans with sans-serif fallback; this finding is the inherited generic fallback" + } + ] + } +} diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..07077e7 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,29 @@ +# Акылдаш — инструкции для агента + +Проект: юридическая информационно-аналитическая платформа; Search API на +Python, SQLite и OpenSearch. Production пока не развернут. + +- Начинай с [инфраструктурного состояния](docs/operations/CURRENT_STATE.md) и + [правил доступа](docs/operations/ACCESS.md). Не запрашивай доступы повторно. +- Архитектура и процедуры: [инфраструктура](docs/operations/INFRASTRUCTURE.md), + [деплой](docs/operations/DEPLOYMENT.md), [backup](docs/operations/BACKUP.md), + [rollback](docs/operations/ROLLBACK.md), [восстановление](docs/operations/RECOVERY.md). +- Работай в feature/fix/refactor-ветке. `main` защищена; изменения вноси через + PR, не сливай его автоматически. Сохраняй незакоммиченные изменения. +- SSH alias — `akyldash_vps`; используй только read-only диагностику. Не давай + `codex` общий sudo или доступ к Docker socket. Root-действия требует владелец. + Для Gitea используй `gitea-api`, не читай файлы с учётными данными. +- Локальные проверки: `PYTHONPATH=backend python3 -m unittest discover -s backend -p 'test_*.py' -v` и + `python3 -m unittest discover -s tools/telegram-bot -p 'test_*.py' -v`, + `python3 -m unittest discover -s infra/backup -p 'test_*.py' -v` и + `python3 -m unittest discover -s infra/deploy -p 'test_*.py' -v`, + `bash infra/bootstrap/test-host-guard.sh`. + Production images собираются командами из `.gitea/workflows/ci.yaml`. +- Перед изменениями production необходим проверенный backup. Формального + механизма миграций нет; проверяй совместимость SQLite/OpenSearch. Не меняй + SSH, firewall или production вручную без владельца и последующего PR. +- Холодный backup требует согласованного окна простоя; восстановление допускается + только в новый staging-каталог, пока владелец отдельно не проверил процедуру + для нового хоста. +- После каждого изменения инфраструктуры обновляй + `docs/operations/CURRENT_STATE.md` по фактическим проверкам. diff --git a/CHANGELOG.md b/CHANGELOG.md index f9ba8ac..d40a38c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## Не выпущено +- Backend `0.11.3`: ключ подписи поисковых снимков читается из Docker secret-файла, а не из аргументов процесса. Добавлена базовая инфраструктура VPS, Caddy HTTPS proxy с разделёнными Compose-сетями и CI-проверка конфигурации; публикация образов и production-деплой остаются выключены до проверки runner, registry, backup/restore и ограниченного deploy-helper. - Frontend `0.1.2`: на планшетной ширине экран поиска переходит в одну колонку, чтобы фильтры, результаты и документ помещались в окно. - Backend `0.11.2`: смена пароля требует текущий пароль и запрещает оставить прежний, включая временный пароль при первом входе. - Backend `0.11.1` и frontend `0.1.1`: в управлении пользователями показываются даты первого и последнего входа, а также последнего сброса пароля. diff --git a/README.md b/README.md index 89fe752..58ab1d4 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ Telegram-бот — только часть рабочего окружения ## Текущее состояние Сейчас реализованы Telegram-бот-секретарь версии `0.2.2`, backend версии -`0.11.2` и внутренний frontend лаборатории `0.1.2`. Авторизация хранится в +`0.11.3` и внутренний frontend лаборатории `0.1.2`. Авторизация хранится в SQLite; первый пользователь `admin` создаётся интерактивной CLI-командой. Поиск расширяет аббревиатуры организационно-правовых форм и поднимает действующий Налоговый кодекс для запросов о налоге на прибыль. @@ -18,7 +18,7 @@ SQLite; первый пользователь `admin` создаётся инт | Компонент | Версия | Состояние | |---|---:|---| | Telegram-бот | `0.2.2` | на Synology работает `0.2.1`; обновление после слияния | -| Backend | `0.11.2` | авторизация и поиск с фильтрами; тематический поиск ОсОО/ЖЧК | +| Backend | `0.11.3` | авторизация и поиск с фильтрами; тематический поиск ОсОО/ЖЧК | | Frontend | `0.1.2` | лаборатория поиска, личный кабинет и управление учетными записями | | Сбор и обработка правовых данных | `0.8.0` | добавлены relevance set и оценка выдачи | | RAG и база знаний | — | ещё не созданы | @@ -62,4 +62,4 @@ python3 -m unittest discover -s tools/telegram-bot -v --- -Акылдаш · Telegram-бот v0.2.2 · Backend v0.11.2 · Frontend v0.1.2 +Акылдаш · Telegram-бот v0.2.2 · Backend v0.11.3 · Frontend v0.1.2 diff --git a/backend/README.md b/backend/README.md index fb4bb2d..7d03fb0 100644 --- a/backend/README.md +++ b/backend/README.md @@ -1,6 +1,6 @@ # Backend Акылдаш -Версия: `0.11.2` +Версия: `0.11.3` Первая backend-область проекта — загрузка правовых документов из официального Open Data API ЦБД Минюста Кыргызской Республики. Код расположен в @@ -253,4 +253,4 @@ PYTHONPATH=backend python3 -m search.query "ЖЧК ачуу тартиби" --la --- -Акылдаш · Backend v0.11.2 · Frontend v0.1.2 +Акылдаш · Backend v0.11.3 · Frontend v0.1.2 diff --git a/backend/search/api.py b/backend/search/api.py index df96ae6..37337ef 100644 --- a/backend/search/api.py +++ b/backend/search/api.py @@ -451,6 +451,7 @@ def main() -> int: parser.add_argument("--reviews-db", type=Path, default=Path("data/search-reviews.sqlite3")) parser.add_argument("--users-db", type=Path, default=Path("data/search-users.sqlite3")) parser.add_argument("--review-secret", default=None) + parser.add_argument("--review-secret-file", type=Path) parser.add_argument("--cookie-secure", action=argparse.BooleanOptionalAction, default=os.environ.get("COOKIE_SECURE", "false").lower() == "true") parser.add_argument("--create-admin", action="store_true", help="create the initial admin account and exit") parser.add_argument("--reset-admin-password", action="store_true", help="set a temporary password for admin and exit") @@ -458,7 +459,14 @@ def main() -> int: parser.add_argument("--port", type=int, default=8080) parser.add_argument("--version", action="version", version=APP_VERSION) arguments = parser.parse_args() - secret = arguments.review_secret.encode() if arguments.review_secret else None + if arguments.review_secret and arguments.review_secret_file: + parser.error("choose either --review-secret or --review-secret-file") + try: + secret = arguments.review_secret.encode() if arguments.review_secret else arguments.review_secret_file.read_bytes().strip() if arguments.review_secret_file else None + except OSError as error: + parser.error(f"cannot read review secret file: {error}") + if arguments.review_secret_file and not secret: + parser.error("review secret file is empty") api = Api(arguments.url, arguments.index, arguments.data, arguments.reviews_db, secret, arguments.users_db, arguments.cookie_secure) if arguments.create_admin or arguments.reset_admin_password: users = api.auth_store.list_users() diff --git a/backend/search/minjust_opensearch.py b/backend/search/minjust_opensearch.py index 8af8236..f62a681 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.11.2" +APP_VERSION = "0.11.3" 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 97bd25c..5817921 100644 --- a/backend/search/review.html +++ b/backend/search/review.html @@ -5,7 +5,7 @@ Оценка поисковой выдачи · Акылдаш