У админки появился собственный сервис, а не только место в плане
Каркас FastAPI живёт в admin/ и собирается отдельным образом: главная страница со списком будущих разделов и /healthz для healthcheck, пока без БД и логина. Сервис добавлен в оба compose (порт только на loopback до появления домена), CI теперь собирает и публикует два образа — бота и админку. Настройки админки наследуют общие, но не требуют BOT_TOKEN: сервис запускается, даже пока токен не заполнен.
This commit is contained in:
parent
16bd8c06b5
commit
b11676764e
|
|
@ -2,6 +2,10 @@
|
||||||
.gitignore
|
.gitignore
|
||||||
.gitattributes
|
.gitattributes
|
||||||
|
|
||||||
|
# секреты локального окружения (в образ не попадают)
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
|
||||||
# venv / кеши
|
# venv / кеши
|
||||||
.venv
|
.venv
|
||||||
.venv11
|
.venv11
|
||||||
|
|
|
||||||
|
|
@ -42,3 +42,6 @@ CLICKHOUSE_PASSWORD=
|
||||||
ANALYTICS_BATCH_SIZE=200
|
ANALYTICS_BATCH_SIZE=200
|
||||||
ANALYTICS_FLUSH_INTERVAL=5.0
|
ANALYTICS_FLUSH_INTERVAL=5.0
|
||||||
ANALYTICS_BUFFER_MAX=5000
|
ANALYTICS_BUFFER_MAX=5000
|
||||||
|
|
||||||
|
# Админка (FastAPI, Блок 4). Порт на loopback хоста для docker compose.
|
||||||
|
ADMIN_HTTP_PORT=8000
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,8 @@ concurrency:
|
||||||
|
|
||||||
env:
|
env:
|
||||||
REGISTRY: git.mpakosrv.ru
|
REGISTRY: git.mpakosrv.ru
|
||||||
IMAGE_NAME: mpakobecex/dispelevilbot-bot
|
IMAGE_NAME_BOT: mpakobecex/dispelevilbot-bot
|
||||||
|
IMAGE_NAME_ADMIN: mpakobecex/dispelevilbot-admin
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
lint-test:
|
lint-test:
|
||||||
|
|
@ -40,16 +41,22 @@ jobs:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- name: Логин в Gitea Registry
|
- name: Логин в Gitea Registry
|
||||||
run: echo "${{ secrets.REGISTRY_TOKEN }}" | docker login "$REGISTRY" -u "${{ secrets.REGISTRY_USER }}" --password-stdin
|
run: echo "${{ secrets.REGISTRY_TOKEN }}" | docker login "$REGISTRY" -u "${{ secrets.REGISTRY_USER }}" --password-stdin
|
||||||
- name: Сборка и публикация образа
|
- name: Сборка и публикация образов
|
||||||
run: |
|
run: |
|
||||||
set -e
|
set -e
|
||||||
IMAGE="$REGISTRY/$IMAGE_NAME"
|
SHORT_SHA="$(echo "${GITHUB_SHA:-unknown}" | cut -c1-12)"
|
||||||
SHA="${GITHUB_SHA:-unknown}"
|
|
||||||
SHORT_SHA="$(echo "$SHA" | cut -c1-12)"
|
|
||||||
VERSION="$(sed -n 's/^__version__ = "\(.*\)"/\1/p' common/version.py)"
|
VERSION="$(sed -n 's/^__version__ = "\(.*\)"/\1/p' common/version.py)"
|
||||||
TAGS="$IMAGE:$SHORT_SHA $IMAGE:latest"
|
|
||||||
if [ -n "$VERSION" ]; then TAGS="$TAGS $IMAGE:$VERSION"; fi
|
build_push() {
|
||||||
ARGS=""
|
image="$1"
|
||||||
for t in $TAGS; do ARGS="$ARGS -t $t"; done
|
dockerfile="$2"
|
||||||
docker build $ARGS .
|
tags="$image:$SHORT_SHA $image:latest"
|
||||||
for t in $TAGS; do docker push "$t"; done
|
if [ -n "$VERSION" ]; then tags="$tags $image:$VERSION"; fi
|
||||||
|
args=""
|
||||||
|
for t in $tags; do args="$args -t $t"; done
|
||||||
|
docker build $args -f "$dockerfile" .
|
||||||
|
for t in $tags; do docker push "$t"; done
|
||||||
|
}
|
||||||
|
|
||||||
|
build_push "$REGISTRY/$IMAGE_NAME_BOT" Dockerfile
|
||||||
|
build_push "$REGISTRY/$IMAGE_NAME_ADMIN" admin/Dockerfile
|
||||||
|
|
|
||||||
19
DEPLOY.md
19
DEPLOY.md
|
|
@ -21,6 +21,7 @@ cp env/test.env.example env/test.env
|
||||||
|
|
||||||
Порт ClickHouse на хосте (`CLICKHOUSE_HTTP_PORT`) у контуров должен различаться:
|
Порт ClickHouse на хосте (`CLICKHOUSE_HTTP_PORT`) у контуров должен различаться:
|
||||||
`prod` — `8123`, `test` — `8124` (иначе контейнеры конфликтуют на одной VPS).
|
`prod` — `8123`, `test` — `8124` (иначе контейнеры конфликтуют на одной VPS).
|
||||||
|
Порт админки (`ADMIN_HTTP_PORT`) — так же: `prod` — `8000`, `test` — `8001`.
|
||||||
|
|
||||||
## 3. Логин в registry (если пакет приватный)
|
## 3. Логин в registry (если пакет приватный)
|
||||||
|
|
||||||
|
|
@ -52,7 +53,8 @@ docker compose -p test --env-file env/test.env -f docker-compose.prod.yml logs -
|
||||||
2. `migrate` (**Flyway**) накатывает миграции из `sql/` — для пустой БД с нуля;
|
2. `migrate` (**Flyway**) накатывает миграции из `sql/` — для пустой БД с нуля;
|
||||||
3. `clickhouse` поднимается и применяет `clickhouse/init/001_events.sql` —
|
3. `clickhouse` поднимается и применяет `clickhouse/init/001_events.sql` —
|
||||||
**только при первой инициализации** пустого data-каталога;
|
**только при первой инициализации** пустого data-каталога;
|
||||||
4. `bot` стартует (`Запуск DispelEvilBot v<версия> (APP_ENV=...)`), аналитика и рассылки — в нём же.
|
4. `bot` стартует (`Запуск DispelEvilBot v<версия> (APP_ENV=...)`), аналитика и рассылки — в нём же;
|
||||||
|
5. `admin` стартует и отвечает на `GET /healthz` (порт на loopback хоста).
|
||||||
|
|
||||||
Проверка контура (сервисы, миграции, Postgres, ClickHouse):
|
Проверка контура (сервисы, миграции, Postgres, ClickHouse):
|
||||||
|
|
||||||
|
|
@ -79,6 +81,21 @@ docker compose -p test --env-file env/test.env -f docker-compose.prod.yml exec -
|
||||||
Изменения схемы: `clickhouse/init/*.sql` применяются лишь на пустом data-каталоге,
|
Изменения схемы: `clickhouse/init/*.sql` применяются лишь на пустом data-каталоге,
|
||||||
поэтому на существующем контуре DDL нужно применить вручную (`make ch E=...`).
|
поэтому на существующем контуре DDL нужно применить вручную (`make ch E=...`).
|
||||||
|
|
||||||
|
## Админка (FastAPI)
|
||||||
|
|
||||||
|
Сервис `admin` — отдельный образ `mpakobecex/dispelevilbot-admin`, общий пакет `common`.
|
||||||
|
Наружу пока **не публикуется**: только loopback хоста (`127.0.0.1:${ADMIN_HTTP_PORT}`).
|
||||||
|
Домен и TLS появятся вместе с входом через Telegram (Блоки 2.4/4.2).
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# проверка живости
|
||||||
|
curl -s http://127.0.0.1:8001/healthz # test
|
||||||
|
curl -s http://127.0.0.1:8000/healthz # prod
|
||||||
|
```
|
||||||
|
|
||||||
|
Локально без Docker: `pip install -r requirements-admin.txt && python -m admin`
|
||||||
|
(по умолчанию `0.0.0.0:8000`, переопределяется `ADMIN_HOST`/`ADMIN_PORT`).
|
||||||
|
|
||||||
## Бэкап событий (забирает домашний сервер)
|
## Бэкап событий (забирает домашний сервер)
|
||||||
|
|
||||||
Сырые события живут на VPS ограниченное время: раз в сутки домашний сервер **сам забирает**
|
Сырые события живут на VPS ограниченное время: раз в сутки домашний сервер **сам забирает**
|
||||||
|
|
|
||||||
12
README.md
12
README.md
|
|
@ -8,6 +8,7 @@ Telegram-бот клана Dispel Evil (aiogram 3 + Postgres). Монорепо
|
||||||
```
|
```
|
||||||
common/ # общий пакет: config, logging, db, repositories, models, texts, analytics
|
common/ # общий пакет: config, logging, db, repositories, models, texts, analytics
|
||||||
bot/ # сервис бота: handlers, services, keyboards, middlewares, notifications
|
bot/ # сервис бота: handlers, services, keyboards, middlewares, notifications
|
||||||
|
admin/ # сервис админки: FastAPI + Jinja2/HTMX (Блок 4), свой Dockerfile
|
||||||
sql/ # миграции Flyway (V__*.sql, R__*.sql)
|
sql/ # миграции Flyway (V__*.sql, R__*.sql)
|
||||||
clickhouse/ # init-скрипты ClickHouse (схема аналитики)
|
clickhouse/ # init-скрипты ClickHouse (схема аналитики)
|
||||||
docs/ # документация (docs/events.md — события аналитики)
|
docs/ # документация (docs/events.md — события аналитики)
|
||||||
|
|
@ -43,6 +44,9 @@ APP_ENV=prod python -m bot # prod
|
||||||
|
|
||||||
или через `./start.sh` (режим берётся из `APP_ENV`).
|
или через `./start.sh` (режим берётся из `APP_ENV`).
|
||||||
|
|
||||||
|
Админка (FastAPI) запускается отдельным процессом: `python -m admin` (зависимости —
|
||||||
|
`requirements-admin.txt`), по умолчанию слушает `0.0.0.0:8000`, проверка живучести — `GET /healthz`.
|
||||||
|
|
||||||
## База данных
|
## База данных
|
||||||
|
|
||||||
Схема управляется миграциями **Flyway** в `sql/` (`V__*.sql` — versioned, `R__*.sql` — repeatable).
|
Схема управляется миграциями **Flyway** в `sql/` (`V__*.sql` — versioned, `R__*.sql` — repeatable).
|
||||||
|
|
@ -66,13 +70,17 @@ APP_ENV=prod python -m bot # prod
|
||||||
|
|
||||||
## Docker
|
## Docker
|
||||||
|
|
||||||
Локальный запуск (postgres + migrate + clickhouse + bot):
|
Локальный запуск (postgres + migrate + clickhouse + bot + admin):
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cp .env.example .env # заполнить
|
cp .env.example .env # заполнить
|
||||||
docker compose up --build
|
docker compose up --build
|
||||||
```
|
```
|
||||||
|
|
||||||
|
CI собирает два образа: `dispelevilbot-bot` и `dispelevilbot-admin`. Админка в compose
|
||||||
|
публикуется только на loopback (`127.0.0.1:${ADMIN_HTTP_PORT}`), наружу выйдет вместе
|
||||||
|
с доменом и TLS (Блок 2.4/4.2).
|
||||||
|
|
||||||
Прод/test-контуры — один `docker-compose.prod.yml`, разделяются Compose-проектом и env-файлом:
|
Прод/test-контуры — один `docker-compose.prod.yml`, разделяются Compose-проектом и env-файлом:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|
@ -104,7 +112,7 @@ make backup E=test # на домашнем сервере: ./scripts/clickho
|
||||||
|
|
||||||
## Версионирование
|
## Версионирование
|
||||||
|
|
||||||
- Единый источник версии — `common/version.py` (`__version__`), текущая — `0.3.9`.
|
- Единый источник версии — `common/version.py` (`__version__`), текущая — `0.4.0`.
|
||||||
- Схема — SemVer-подобная (`MAJOR.MINOR.PATCH`); git-тег = `v` + версия (`v0.1`).
|
- Схема — SemVer-подобная (`MAJOR.MINOR.PATCH`); git-тег = `v` + версия (`v0.1`).
|
||||||
- Docker-образы тегируются версией (Блок 2), события ClickHouse получают `bot_version` (Блок 3).
|
- Docker-образы тегируются версией (Блок 2), события ClickHouse получают `bot_version` (Блок 3).
|
||||||
- Выпуск: изменить `common/version.py` → коммит → аннотированный тег `vX.Y` → `git push --tags`.
|
- Выпуск: изменить `common/version.py` → коммит → аннотированный тег `vX.Y` → `git push --tags`.
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,29 @@
|
||||||
|
# syntax=docker/dockerfile:1
|
||||||
|
# Образ сервиса админки (FastAPI + Jinja2/HTMX).
|
||||||
|
FROM python:3.12-slim
|
||||||
|
|
||||||
|
ENV PYTHONUNBUFFERED=1 \
|
||||||
|
PYTHONDONTWRITEBYTECODE=1 \
|
||||||
|
PIP_NO_CACHE_DIR=1 \
|
||||||
|
PIP_DISABLE_PIP_VERSION_CHECK=1
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Непривилегированный пользователь
|
||||||
|
RUN useradd --create-home --uid 1000 app
|
||||||
|
|
||||||
|
COPY requirements-admin.txt ./
|
||||||
|
RUN pip install --no-cache-dir -r requirements-admin.txt
|
||||||
|
|
||||||
|
COPY common ./common
|
||||||
|
COPY admin ./admin
|
||||||
|
|
||||||
|
USER app
|
||||||
|
|
||||||
|
EXPOSE 8000
|
||||||
|
|
||||||
|
# Живость процесса: HTTP-проверка /healthz (порт берётся из ADMIN_PORT).
|
||||||
|
HEALTHCHECK --interval=30s --timeout=5s --start-period=20s --retries=3 \
|
||||||
|
CMD python -c "import os,urllib.request; urllib.request.urlopen('http://127.0.0.1:'+os.environ.get('ADMIN_PORT','8000')+'/healthz', timeout=3)"
|
||||||
|
|
||||||
|
CMD ["python", "-m", "admin"]
|
||||||
|
|
@ -0,0 +1,4 @@
|
||||||
|
"""Сервис админки DispelEvilBot (FastAPI + HTMX).
|
||||||
|
|
||||||
|
Отдельный процесс/образ; общий код — пакет ``common``. Запуск: ``python -m admin``.
|
||||||
|
"""
|
||||||
|
|
@ -0,0 +1,31 @@
|
||||||
|
"""Точка входа админки: python -m admin"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import logging
|
||||||
|
|
||||||
|
import uvicorn
|
||||||
|
|
||||||
|
from common.config import get_admin_settings
|
||||||
|
from common.logging_config import setup_logging
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
"""Запустить uvicorn с приложением из ``admin.app``."""
|
||||||
|
setup_logging(logging.INFO)
|
||||||
|
settings = get_admin_settings()
|
||||||
|
logger.info("Запуск админки (APP_ENV=%s) на %s:%s", settings.app_env, settings.admin_host, settings.admin_port)
|
||||||
|
# log_config=None — чтобы uvicorn не перетирал единый формат логов.
|
||||||
|
uvicorn.run(
|
||||||
|
"admin.app:create_app",
|
||||||
|
factory=True,
|
||||||
|
host=settings.admin_host,
|
||||||
|
port=settings.admin_port,
|
||||||
|
log_config=None,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
|
|
@ -0,0 +1,40 @@
|
||||||
|
"""Создание FastAPI-приложения админки."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from fastapi import FastAPI
|
||||||
|
from fastapi.templating import Jinja2Templates
|
||||||
|
|
||||||
|
from admin.routes import health, pages
|
||||||
|
from common import __version__
|
||||||
|
from common.config import AdminSettings, get_admin_settings
|
||||||
|
from common.logging_config import setup_logging
|
||||||
|
|
||||||
|
BASE_DIR = Path(__file__).resolve().parent
|
||||||
|
TEMPLATES_DIR = BASE_DIR / "templates"
|
||||||
|
|
||||||
|
|
||||||
|
def create_app(settings: AdminSettings | None = None) -> FastAPI:
|
||||||
|
"""Собрать приложение: настройки, шаблоны, роутеры.
|
||||||
|
|
||||||
|
Настройки можно передать явно (тесты); по умолчанию читаются из окружения.
|
||||||
|
"""
|
||||||
|
setup_logging()
|
||||||
|
settings = settings or get_admin_settings()
|
||||||
|
|
||||||
|
app = FastAPI(
|
||||||
|
title="DispelEvilBot Admin",
|
||||||
|
version=__version__,
|
||||||
|
# Swagger/OpenAPI наружу пока не нужны: админка закрыта логином (Блок 4.2).
|
||||||
|
docs_url=None,
|
||||||
|
redoc_url=None,
|
||||||
|
openapi_url=None,
|
||||||
|
)
|
||||||
|
app.state.settings = settings
|
||||||
|
app.state.templates = Jinja2Templates(directory=str(TEMPLATES_DIR))
|
||||||
|
|
||||||
|
app.include_router(health.router)
|
||||||
|
app.include_router(pages.router)
|
||||||
|
return app
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
"""Роутеры админки."""
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
"""Служебные эндпоинты: healthcheck для Docker и мониторинга."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from fastapi import APIRouter, Request
|
||||||
|
|
||||||
|
from common import __version__
|
||||||
|
|
||||||
|
router = APIRouter(tags=["service"])
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/healthz")
|
||||||
|
async def healthz(request: Request) -> dict[str, str]:
|
||||||
|
"""Вернуть состояние сервиса (используется healthcheck'ом контейнера)."""
|
||||||
|
settings = request.app.state.settings
|
||||||
|
return {"status": "ok", "version": __version__, "app_env": settings.app_env}
|
||||||
|
|
@ -0,0 +1,34 @@
|
||||||
|
"""HTML-страницы админки (серверный рендер Jinja2 + HTMX)."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from fastapi import APIRouter, Request
|
||||||
|
from fastapi.responses import HTMLResponse
|
||||||
|
|
||||||
|
from common import __version__
|
||||||
|
|
||||||
|
router = APIRouter(tags=["pages"])
|
||||||
|
|
||||||
|
#: Разделы админки по плану Блока 4 (пока заглушки на главной).
|
||||||
|
SECTIONS: tuple[tuple[str, str, str], ...] = (
|
||||||
|
("messages", "Сообщения", "Тексты бота и предпросмотр с тегами {tag}"),
|
||||||
|
("schedules", "Расписания", "Время рассылок, дни недели и шаблон"),
|
||||||
|
("users", "Пользователи", "Ники, звания, баланс и подписка"),
|
||||||
|
("logs", "Логи", "События из ClickHouse с фильтрами"),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/", response_class=HTMLResponse)
|
||||||
|
async def index(request: Request) -> HTMLResponse:
|
||||||
|
"""Главная страница: версия, окружение и список будущих разделов."""
|
||||||
|
templates = request.app.state.templates
|
||||||
|
settings = request.app.state.settings
|
||||||
|
return templates.TemplateResponse(
|
||||||
|
request=request,
|
||||||
|
name="index.html",
|
||||||
|
context={
|
||||||
|
"version": __version__,
|
||||||
|
"app_env": settings.app_env,
|
||||||
|
"sections": SECTIONS,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="ru">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<title>{% block title %}Админка{% endblock %} — DispelEvilBot</title>
|
||||||
|
<script src="https://cdn.tailwindcss.com"></script>
|
||||||
|
<script src="https://unpkg.com/htmx.org@2.0.4" defer></script>
|
||||||
|
</head>
|
||||||
|
<body class="min-h-screen bg-slate-100 text-slate-900">
|
||||||
|
<header class="border-b border-slate-200 bg-white">
|
||||||
|
<div class="mx-auto flex max-w-5xl items-center justify-between px-4 py-4">
|
||||||
|
<a href="/" class="text-lg font-semibold">DispelEvilBot</a>
|
||||||
|
<span class="text-sm text-slate-500">админка · v{{ version }}</span>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main class="mx-auto max-w-5xl px-4 py-8">
|
||||||
|
{% block content %}{% endblock %}
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<footer class="mx-auto max-w-5xl px-4 pb-8 text-xs text-slate-400">DispelEvilBot v{{ version }}</footer>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}Главная{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<h1 class="text-2xl font-semibold">Панель администрирования</h1>
|
||||||
|
<p class="mt-2 text-slate-600">
|
||||||
|
Окружение: <span class="font-mono">{{ app_env }}</span>. Это каркас админки: разделы
|
||||||
|
появятся по мере готовности (Блок 4.1–4.3).
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="mt-8 grid gap-4 sm:grid-cols-2">
|
||||||
|
{% for key, title, description in sections %}
|
||||||
|
<section class="rounded-lg border border-slate-200 bg-white p-5">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<h2 class="text-lg font-medium">{{ title }}</h2>
|
||||||
|
<span class="rounded bg-slate-100 px-2 py-0.5 text-xs text-slate-500">скоро</span>
|
||||||
|
</div>
|
||||||
|
<p class="mt-2 text-sm text-slate-600">{{ description }}</p>
|
||||||
|
</section>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
@ -105,5 +105,26 @@ class Settings(BaseSettings):
|
||||||
|
|
||||||
@lru_cache
|
@lru_cache
|
||||||
def get_settings() -> Settings:
|
def get_settings() -> Settings:
|
||||||
"""Вернуть singleton настроек."""
|
"""Вернуть singleton настроек бота."""
|
||||||
return Settings() # type: ignore[call-arg]
|
return Settings() # type: ignore[call-arg]
|
||||||
|
|
||||||
|
|
||||||
|
class AdminSettings(Settings):
|
||||||
|
"""Настройки сервиса админки.
|
||||||
|
|
||||||
|
Наследует общие поля (окружение, Postgres, таймзона, ClickHouse), но не требует
|
||||||
|
``BOT_TOKEN``: на этапе каркаса он не нужен, а появится в проверке подписи
|
||||||
|
Telegram-логина (Блок 4.2).
|
||||||
|
"""
|
||||||
|
|
||||||
|
bot_token: str = ""
|
||||||
|
|
||||||
|
#: Адрес и порт, которые слушает uvicorn внутри контейнера.
|
||||||
|
admin_host: str = "0.0.0.0"
|
||||||
|
admin_port: int = 8000
|
||||||
|
|
||||||
|
|
||||||
|
@lru_cache
|
||||||
|
def get_admin_settings() -> AdminSettings:
|
||||||
|
"""Вернуть singleton настроек админки."""
|
||||||
|
return AdminSettings() # type: ignore[call-arg]
|
||||||
|
|
|
||||||
|
|
@ -7,4 +7,4 @@
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
__version__ = "0.3.9"
|
__version__ = "0.4.0"
|
||||||
|
|
|
||||||
|
|
@ -62,6 +62,31 @@ services:
|
||||||
max-size: "10m"
|
max-size: "10m"
|
||||||
max-file: "3"
|
max-file: "3"
|
||||||
|
|
||||||
|
# Админка (FastAPI + Jinja2/HTMX), образ из Gitea Registry.
|
||||||
|
# Наружу пока не публикуется: только loopback хоста; домен/TLS — Блок 2.4/4.2.
|
||||||
|
admin:
|
||||||
|
image: ${REGISTRY:-git.mpakosrv.ru}/mpakobecex/dispelevilbot-admin:${IMAGE_TAG:-latest}
|
||||||
|
restart: unless-stopped
|
||||||
|
env_file:
|
||||||
|
- ${ENV_FILE:-env/prod.env}
|
||||||
|
environment:
|
||||||
|
DB_HOST: postgres
|
||||||
|
DB_PORT: "5432"
|
||||||
|
ADMIN_HOST: 0.0.0.0
|
||||||
|
ADMIN_PORT: "8000"
|
||||||
|
ports:
|
||||||
|
- "127.0.0.1:${ADMIN_HTTP_PORT:-8000}:8000"
|
||||||
|
depends_on:
|
||||||
|
postgres:
|
||||||
|
condition: service_healthy
|
||||||
|
migrate:
|
||||||
|
condition: service_completed_successfully
|
||||||
|
logging:
|
||||||
|
driver: json-file
|
||||||
|
options:
|
||||||
|
max-size: "10m"
|
||||||
|
max-file: "3"
|
||||||
|
|
||||||
# Аналитика событий (docs/events.md). Наружу не публикуется:
|
# Аналитика событий (docs/events.md). Наружу не публикуется:
|
||||||
# только loopback хоста для бэкапа/туннеля; бот ходит внутрь сети compose.
|
# только loopback хоста для бэкапа/туннеля; бот ходит внутрь сети compose.
|
||||||
clickhouse:
|
clickhouse:
|
||||||
|
|
|
||||||
|
|
@ -67,6 +67,34 @@ services:
|
||||||
max-size: "10m"
|
max-size: "10m"
|
||||||
max-file: "3"
|
max-file: "3"
|
||||||
|
|
||||||
|
# Админка (FastAPI + HTMX). Порт — только loopback: наружу выйдет через reverse proxy
|
||||||
|
# вместе с TLS и доменом (Блок 2.4/4.2).
|
||||||
|
admin:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: admin/Dockerfile
|
||||||
|
image: dispelevilbot-admin:local
|
||||||
|
restart: unless-stopped
|
||||||
|
env_file:
|
||||||
|
- .env
|
||||||
|
environment:
|
||||||
|
DB_HOST: postgres
|
||||||
|
DB_PORT: "5432"
|
||||||
|
ADMIN_HOST: 0.0.0.0
|
||||||
|
ADMIN_PORT: "8000"
|
||||||
|
ports:
|
||||||
|
- "127.0.0.1:${ADMIN_HTTP_PORT:-8000}:8000"
|
||||||
|
depends_on:
|
||||||
|
postgres:
|
||||||
|
condition: service_healthy
|
||||||
|
migrate:
|
||||||
|
condition: service_completed_successfully
|
||||||
|
logging:
|
||||||
|
driver: json-file
|
||||||
|
options:
|
||||||
|
max-size: "10m"
|
||||||
|
max-file: "3"
|
||||||
|
|
||||||
# Аналитика событий (Блок 3). DDL — в clickhouse/init/.
|
# Аналитика событий (Блок 3). DDL — в clickhouse/init/.
|
||||||
clickhouse:
|
clickhouse:
|
||||||
image: clickhouse/clickhouse-server:25.3
|
image: clickhouse/clickhouse-server:25.3
|
||||||
|
|
|
||||||
|
|
@ -39,3 +39,7 @@ CLICKHOUSE_USER=de_bot
|
||||||
CLICKHOUSE_PASSWORD=
|
CLICKHOUSE_PASSWORD=
|
||||||
# Порт на хосте (для бэкапа). У test и prod он должен различаться.
|
# Порт на хосте (для бэкапа). У test и prod он должен различаться.
|
||||||
CLICKHOUSE_HTTP_PORT=8123
|
CLICKHOUSE_HTTP_PORT=8123
|
||||||
|
|
||||||
|
# Админка (FastAPI). Порт на loopback хоста; у test и prod должен различаться.
|
||||||
|
# Наружу пока не публикуется (домен/TLS — Блок 2.4).
|
||||||
|
ADMIN_HTTP_PORT=8000
|
||||||
|
|
|
||||||
|
|
@ -37,3 +37,7 @@ CLICKHOUSE_USER=de_bot
|
||||||
CLICKHOUSE_PASSWORD=
|
CLICKHOUSE_PASSWORD=
|
||||||
# Порт на хосте: у test он отличается от prod (8123), иначе конфликт на одной VPS.
|
# Порт на хосте: у test он отличается от prod (8123), иначе конфликт на одной VPS.
|
||||||
CLICKHOUSE_HTTP_PORT=8124
|
CLICKHOUSE_HTTP_PORT=8124
|
||||||
|
|
||||||
|
# Админка (FastAPI). Порт на loopback хоста; у test и prod должен различаться.
|
||||||
|
# Наружу пока не публикуется (домен/TLS — Блок 2.4).
|
||||||
|
ADMIN_HTTP_PORT=8001
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,15 @@
|
||||||
|
# Зависимости сервиса админки (FastAPI). Бот использует requirements.txt.
|
||||||
|
# Python 3.12
|
||||||
|
|
||||||
|
fastapi>=0.115,<1.0
|
||||||
|
uvicorn[standard]>=0.30,<1.0
|
||||||
|
jinja2>=3.1,<4.0
|
||||||
|
|
||||||
|
pydantic>=2.7,<3.0
|
||||||
|
pydantic-settings>=2.3,<3.0
|
||||||
|
|
||||||
|
# Postgres (async psycopg v3) — понадобится разделам шаблонов и пользователей.
|
||||||
|
psycopg[binary,pool]>=3.2,<4.0
|
||||||
|
|
||||||
|
# Разбор form-data (HTMX-формы).
|
||||||
|
python-multipart>=0.0.9,<1.0
|
||||||
|
|
@ -5,3 +5,5 @@ black>=24.4,<25.0
|
||||||
pytest>=8.2,<9.0
|
pytest>=8.2,<9.0
|
||||||
pytest-asyncio>=0.23,<1.0
|
pytest-asyncio>=0.23,<1.0
|
||||||
pytest-cov>=5.0,<6.0
|
pytest-cov>=5.0,<6.0
|
||||||
|
# TestClient для FastAPI-админки.
|
||||||
|
httpx>=0.27,<1.0
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,30 @@
|
||||||
|
"""Тесты каркаса админки: healthcheck и главная страница (без БД и сети)."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from fastapi.testclient import TestClient
|
||||||
|
|
||||||
|
from admin.app import create_app
|
||||||
|
from common.config import AdminSettings
|
||||||
|
|
||||||
|
|
||||||
|
def _client() -> TestClient:
|
||||||
|
settings = AdminSettings(_env_file=None, bot_token="", db_password="test", app_env="dev")
|
||||||
|
return TestClient(create_app(settings))
|
||||||
|
|
||||||
|
|
||||||
|
def test_healthz() -> None:
|
||||||
|
response = _client().get("/healthz")
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
payload = response.json()
|
||||||
|
assert payload["status"] == "ok"
|
||||||
|
assert payload["app_env"] == "dev"
|
||||||
|
|
||||||
|
|
||||||
|
def test_index_renders_sections() -> None:
|
||||||
|
response = _client().get("/")
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert "Панель администрирования" in response.text
|
||||||
|
assert "Расписания" in response.text
|
||||||
|
|
@ -7,4 +7,4 @@ from common import __version__
|
||||||
|
|
||||||
def test_version_is_set():
|
def test_version_is_set():
|
||||||
assert __version__
|
assert __version__
|
||||||
assert __version__ == "0.3.9"
|
assert __version__ == "0.4.0"
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue