惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

U
Unit 42
罗磊的独立博客
爱范儿
爱范儿
Apple Machine Learning Research
Apple Machine Learning Research
S
Schneier on Security
V
Vulnerabilities – Threatpost
T
Tenable Blog
博客园 - 【当耐特】
博客园 - 聂微东
I
Intezer
S
Securelist
C
Cisco Blogs
腾讯CDC
T
The Exploit Database - CXSecurity.com
NISL@THU
NISL@THU
P
Proofpoint News Feed
Cyberwarzone
Cyberwarzone
Security Archives - TechRepublic
Security Archives - TechRepublic
D
Darknet – Hacking Tools, Hacker News & Cyber Security
美团技术团队
P
Proofpoint News Feed
M
MIT News - Artificial intelligence
AWS News Blog
AWS News Blog
A
About on SuperTechFans
Last Week in AI
Last Week in AI
Cloudbric
Cloudbric
S
Secure Thoughts
PCI Perspectives
PCI Perspectives
The Last Watchdog
The Last Watchdog
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
B
Blog RSS Feed
Recorded Future
Recorded Future
大猫的无限游戏
大猫的无限游戏
C
CERT Recently Published Vulnerability Notes
酷 壳 – CoolShell
酷 壳 – CoolShell
G
Google Developers Blog
The Register - Security
The Register - Security
博客园 - 叶小钗
博客园 - 司徒正美
博客园_首页
Google DeepMind News
Google DeepMind News
Hacker News: Ask HN
Hacker News: Ask HN
Spread Privacy
Spread Privacy
雷峰网
雷峰网
I
InfoQ
The Hacker News
The Hacker News
T
Threat Research - Cisco Blogs
H
Hackread – Cybersecurity News, Data Breaches, AI and More
Schneier on Security
Schneier on Security
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻

Все публикации подряд на Хабре

Ловим музу за клавиатуру: как айтишнику стать автором Что умеет Midjourney в 2026? Мой немного грустный разбор этого шикарного инструмента Никто не любит писать тесты, но ИИ может исправить это IPv8 выглядит как мечта. Поэтому почти наверняка не взлетит Производители вернули в продажу материнки с DDR3. Что происходит? Управление агентом с телефона через Telegram теперь в KodaCode От координации к лидерству: как меняется роль руководителя разработки Я сделала родителям бизнес вместо пенсии: зарабатываем 70 тысяч, мама не даёт продать В три раза быстрее приемка товара и оптимизация трудозатрат на 73%: как «РСТ-Инвент» помог Gulliver Group ИИ-шечный мир победил? О влиянии искусственного интеллекта на игропром Кремль снижает давление на Телеграмм пока Европа строит интернет по паспорту Как CEO, CTO и CIO за 8 часов собрали ИИ-директора, который умеет держать позицию под давлением Как (не) потерять домен за выходные Вместо 8 разных VPS: как я организовал практику студентам на одном сервере Почему твой Open Source проект не замечают? R&D: искусство управления неопределенностью в разработке AI-дефляция: вакансий для разработчиков больше, а рост зарплат — худший за 15 лет Мы отдали управление роботами OpenClaw. Что из этого вышло Галактический ID: система идентификации для всех форм разумной жизни Шесть основ бизнес-анализа: начинаем с вопроса «Кто в игре?» Код-ревью, в котором дело не в коде Данные переехали. Команда — нет Системной подход к сдаче OSWE в 2025 Почему комната управления реактором покрашена в цвет морской пены 4 YAML-файла вместо PySpark: как аналитикам строить пайплайны без разработчиков LLM-агент для поиска свободных доменов: автоматизируем подбор Когда, зачем и как правильно начинать новую сессию в Claude Code? Как я заставил нейросеть писать макросы для FreeCAD Анатомия ИИ‑агента для подбора персонала. От тысячи резюме к топ‑10 за минуты Опыт разработчика как экономика внимания Автономность как точка невозврата: кто будет субъектом в цифровом будущем Обучение ИИ в «диких» условиях: как рутинные действия превращаются в датасеты Как измерить LLM для задач кибербеза: обзор открытых бенчмарков Где хранить код? Сравнение GitHub, GitLab и Bitbucket Математика объясняет, почему нормальное распределение встречается повсюду Почему ваш FinOps не работает: 12 тезисов от практиков Как подписать проектную документацию УКЭП с использованием бесплатных лицензий Pilot Адаптивное администрирование Sigla Vision Я грузил уран в бочки, а потом 20 лет строил ИТ в атомной отрасли Чем позвонить с Эвереста? История и обзор спутниковой связи. Часть 2 Как языковая модель помогает контролировать качество инструктажей по охране труда в металлургии Как не передать на desktop свой IP в РКН Анатомия SAP Privileges: как устроено управление правами в macOS MoneyDev: Сказка про три главных слова Обновлённый токенизатор видео K-VAE 2.0 от Сбера Как сделать диспетчеризацию дома на 1284 квартиры почти бесплатно Как мы разогнали железную дорогу Мы дали агентам рутину. Теперь надо решить — что делать с освободившимся временем Токсичный контент, промпт-хакинг и защита ИИ — всё о Guardrails для LLM Умный город начинается с точного взгляда: как «Фалькон Тех» меняет пространство к лучшему Навайбкодил приложение для анализа графов Почему Дюну так интересно читать? Упрощаем работу с рутиной или как стать Гендальфом Белым Деконструкция Go: CPU, RAM и что там происходит. Go Assembler база. Часть 1.1 Какие профессии исчезнут из-за ИИ, а какие появятся? И что с этим делать Как мы построили IT-отдел, где хочется расти: архитектурные встречи, прозрачные метрики и книжные подарки Rufler: Делаем из Claude Code автономный рой через один YAML-конфиг Sing-box и белый список приложений Как построить надёжный обмен сообщениями в микросервисах: лучшие практики для enterprise OpenAI строит MLM-пирамиду, а McKinsey и Accenture помогают ей в этом Дом, который не построил Фишер (Часть 2) «Сверхзвуковой математик» против «Вдумчивого логиста»: битва алгоритмов 3D-упаковки Мультимодальные модели – грубый и дорогой инструмент Разговоры ничего не стоят. Код тоже Проверки физических лиц: с кого начнет ФНС Топ-10 бесплатных нейросетей для создания видео в 2026 году Первые слои кода: как наши решения сегодня определяют архитектуру ИИ на десятилетия Разработка нового статического анализатора: PVS-Studio JavaScript Поиск уязвимостей ПО: базовый минимум или роскошный максимум Почему оценка персонала не работает как инструмент управления Как мы разработали ИИ-ассистента и сократили рутину продуктовой команды на 50% Как я ушел из найма, нажарил косточек и продал на маркетплейсах на 168 млн в год Когда 1С:ERP уже внедрена, а нормального производственного плана всё ещё нет Как я сделал Claude мультимодальным, подключив к нему Qwen Omni Как приглашение на вакансию мечты превращается в атаку Infrastructure as Code: философия и лучшие практики IaC Тестируем Yandex Code Assistant на задаче, в которой нужно хранить секреты nxs-universal-chart v3.0: новое поколение универсального Helm-чарта Callback Injection: Техника, которая отправила Microsoft Defender в глухой нокаут «Все идеи на стол»: митап как способ вывести проект из тупика Сегодня я узнал нечто новое о GPU благодаря багу в своей игре Как заставить LLM ̶ ̶г̶а̶л̶л̶ю̶ ̶ эволюционировать Карта событий как фундамент аналитики: практический кейс для E-commerce Что выбрать для AI: x86, ARM или RISC-V? Дайджест железа за март Роль соматических мутаций в развитии аутоиммунных заболеваний: путь к избирательной терапии Mythos от Anthropic — тревожный сигнал для всех, а не только для банков Guardrails для LLM на Java: как приручить промпт‑инъекции и токсичные ответы Green-VLA: как мы собрали VLA-модель для реального антропоморфного робота и не потеряли обобщение Финансовая гонка вооружений: почему умные люди добровольно в ней участвуют Эра ИИ-агентов наступила: выбираем лучшего цифрового сотрудника # Практический опыт внедрения WinCC Redundancy на производственном предприятии Сделал MVP за 3 дня, а потом неделю прикручивал оплату. Оно того стоило? Физика против Маска: почему Starship V3 может оказаться ещё одной катастрофой Нефть Венесуэлы: крупнейшие запасы в мире, но не крупнейшая нефтяная держава JPA 4. Переосмысление Hibernate Почему зеркальная фотокамера Nikon D5 десятилетней давности идеально подошла для миссии «Артемида-2» Проект «Уровень-Спутник» или как мы сделали платформу для гидрологов «Замедлиться, чтобы ускориться»: почему ИИ повышает цену ошибок в требованиях и архитектуре Как с нуля поднять трафик IT-компании на 1657% при бюджете 55 тыс. и выжить Pixel-perfect Downsampling — идеальная отрисовка 50 миллионов точек без потерь
Пять продуктов в одном FastAPI-монолите: HTMX вместо React, грабли Telegram Mini App и биллинг на Stars
SwairIt · 2026-06-07 · via Все публикации подряд на Хабре

10 мин

4.4K

Привет, Хабр. Меня зовут Ярослав, в сети — SwairIt. Полтора месяца назад я начал писать обычный todo-лист на FastAPI, а в итоге под одним доменом getdoday.ru выросла небольшая студия из пяти продуктов: todo-приложение, кабинет для репетиторов, школьное Q&A, тренажёр билетов ПДД и Telegram-игра. Всё это — один FastAPI-монолит без единой строки React, ~76 000 строк кода и 1200+ тестов.

В этой статье я разберу то, что считаю полезным для других:

  • как один FastAPI-проект держит сразу несколько продуктов и не превращается в кашу;

  • почему я выбрал HTMX вместо React и о чём не пожалел;

  • четыре грабли Telegram Mini App, на которые ушли часы, и monkey-patch DNS, оживший бота на проде;

  • неочевидное ограничение биллинга на Telegram Stars и паттерн, который его обходит;

  • как устроен дев-процесс: mypy --strict, ruff, CI и автодеплой за минуту.

Пишу я в паре с Claude Code — терминальным AI-агентом. Не скрываю этого и ниже честно расскажу, как именно выстроен такой процесс. Поехали.

Главная getdoday.ru

Главная getdoday.ru

Что живёт под одним доменом

getdoday.ru — это витрина, с которой ведут ссылки на отдельные продукты. Все они работают в одном процессе, делят базу, инфраструктуру и одного бота:

  • Doday Tasks (/) — кросс-платформенный todo: веб-кабинет, Telegram Mini App и чат-бот. Приоритеты P1–P4, дедлайны, повторения, проекты, секции, kanban, быстрый ввод на естественном языке.

  • Lessio (/lessio) — публичная страница и кабинет для репетиторов: услуги, расписание, запись клиентов и оплата через Telegram Stars.

  • Razbery (/qa/) — школьное Q&A с разборами: 16 предметов, 5–11 класс. Не «готовый ответ», а объяснение. Растёт за счёт органического поиска.

  • Doday ПДД (/pdd/) — тренажёр официальных билетов ГИБДД: 1600 вопросов двух категорий (АВМ и CD), экзамен по правилам, марафон, поиск, статистика ошибок.

  • Tap Tower (/taptower) — небольшая Telegram-игра (Mini App).

Каждый продукт — отдельный «срез» одного кода. Дальше расскажу, как это устроено, но сначала — про стек, потому что именно он делает такую плотность возможной.

Стек: почему не React

Я учу JavaScript медленнее, чем Python, и в момент, когда нужно быстро довезти что-то рабочее, изучение React стало бы тормозом. Поэтому стек я выбирал по принципу «максимум функциональности при минимуме боли на фронте». Получилось так:

Слой

Что выбрал

Почему

Backend

FastAPI + async SQLAlchemy 2.0 + Pydantic v2

Типы везде, mypy --strict зелёный, OpenAPI бесплатно

База

PostgreSQL 16 (asyncpg) + Alembic

Production-grade, миграции, а не SQLite

Шаблоны

Jinja2, server-side render

Никакой гидратации, быстрый first paint

Интерактив

HTMX 2

Свапы кусков HTML по запросу — SPA-ощущение без JSON-API и без бандла

Микросостояние

Alpine.js

x-data, x-show, x-model — небольшие JS-фрагменты прямо в HTML

Стили

Tailwind (CDN)

Ноль конфигурации и сборки

Auth

Свой на argon2 + itsdangerous

Cookie-сессии, без JWT

Логи

structlog (JSON)

Грепаем по chat_id, task_id

Инструменты

uv + ruff + mypy --strict + pre-commit

Зелёный линт на каждом коммите

CI/деплой

GitHub Actions + cron-poll

git push → прод обновляется автоматически за ~60 секунд

Самый спорный выбор — Tailwind через CDN в проде. Да, это медленнее, чем собранный и очищенный CSS, и вес стилей великоват. Но ноль конфигурации, отсутствие сборки и node_modules окупают это на текущей стадии; на сборку через PostCSS я перейду, когда это станет узким местом.

А вот HTMX оказался приятным открытием. Я переписал половину интерфейса с ручного fetch + polling на hx-get + hx-swap и получил более отзывчивый UI, чем у части React-приложений, которые видел до этого. Причина простая: нет сериализации в JSON, нет дифа виртуального DOM, нет парсинга JS — приходит готовый кусок HTML и заменяет узел. На мобильных это особенно заметно.

Один монолит, несколько продуктов

Чтобы пять продуктов в одном репозитории не превратились в спагетти, я держусь двух правил.

Первое — структура по фиче, а не по слою. Не общие папки models/, routers/, services/, а самодостаточные модули:

app/  auth/        {router,service,models,schemas,deps}.py  tasks/       {router,service,models,schemas}.py  billing/     {router,service,models,products,stars}.py  lessio/      ...  qa/          ...  pdd/         {router,service,models,seo,seed_load}.py  telegram/    bot.py  main.py      # тут роутеры собираются вместе

Всё, что относится к одной фиче, лежит рядом. Новый продукт — это новая папка app/<feature>/ и пара строк в main.py:

app.include_router(pdd_router)       # HTML-страницы на /pdd
app.include_router(pdd_api_router)   # JSON-эндпоинты на /api/pdd

Второе — общая инфраструктура переиспользуется, а не копируется. Авторизация (app/auth/deps.py), биллинг на Stars, рассылка писем, генерация sitemap.xml, бот — это общие модули. Когда я делал ПДД, мне не пришлось заново писать ни авторизацию, ни оплату, ни SEO-обвязку: продукт просто подключился к уже готовым кускам. Роутеры монтируются на разные префиксы (/pdd, /qa, /lessio), и каждый продукт получает свой кусок URL-пространства.

Правило «роутер ходит только в сервис, а не в ORM напрямую» помогает держать слои чистыми: вся работа с базой — в service.py, а роутер только собирает контекст и рендерит шаблон.

Telegram Mini App: четыре грабли

Mini App — часть, которой я доволен больше всего, и одновременно та, где граблей оказалось больше, чем кода. Разберу четыре, на которые ушли часы.

Mini App с быстрым вводом, кольцом прогресса и нижними табами

Mini App с быстрым вводом, кольцом прогресса и нижними табами

Грабля №1 — themeParams ломает вашу палитру

Telegram WebApp SDK отдаёт themeParams: цвета фона, текста и акцента из темы пользователя. Кажется логичным взять их и применить к своему интерфейсу, чтобы Mini App «выглядел нативно». Это ловушка.

Если у пользователя Telegram в светлой теме, вы получите bg_color: #ffffff. И все ваши rgba(255,255,255,0.06) для shimmer-эффекта, тонкие границы rgba(255,255,255,0.08), чипы под тёмный фон — на белом превращаются в нечитаемую кашу. Я один раз слепо скопировал themeParams и получил сломанный интерфейс у каждого второго пользователя.

Решение: зафиксировать собственную тему, а пользователю дать явный переключатель «тёмная / светлая / системная». Светлую я переписал с нуля на палитре slate, а не выводил из чужих цветов. Выбор хранится в localStorage; при переключении я зову tg.setHeaderColor(...) и tg.setBackgroundColor(...), чтобы перекрасился и chrome-бар Telegram поверх Mini App. Анти-мерцание — через inline-скрипт в <head> до первого paint.

Грабля №2 — api.telegram.org по IPv6 на проде = смерть бота

Бот живёт на VPS. Системный DNS на проде отдаёт для api.telegram.org адрес, который заблокирован у провайдера, а доступный IPv4 не возвращается. В итоге httpx внутри python-telegram-bot честно резолвит хост, попадает на недоступный адрес, висит в connect-timeout — и бот молча перестаёт отвечать.

Проверка показала, что нужный IPv4 у Telegram есть: curl --resolve api.telegram.org:443:149.154.167.220 https://... работает. Просто DNS отдаёт не то. Sudo на проде нет, /etc/hosts не поправить.

Первое решение — monkey-patch на резолвер. Важная тонкость: патчить надо в двух местах, потому что синхронный и асинхронный резолв идут разными путями:

import asyncio.base_events
import socket
from typing import Any
_TELEGRAM_API_IPS = ("149.154.167.220",)
_FORCED_HOSTS = {"api.telegram.org"}
def _force_ipv4_resolve() -> None:    # 1. socket.getaddrinfo — синхронный резолв (curl-подобные и sync-библиотеки)    sync_orig = socket.getaddrinfo    def _v4_sync(host: Any, *args: Any, **kwargs: Any) -> Any:        if host in _FORCED_HOSTS:            port = args[0] if args else kwargs.get("port", 0)            return [                (socket.AF_INET, socket.SOCK_STREAM, 6, "", (ip, port))                for ip in _TELEGRAM_API_IPS            ]        return sync_orig(host, *args, **kwargs)    socket.getaddrinfo = _v4_sync    # 2. asyncio.BaseEventLoop.getaddrinfo — асинхронный резолв    #    (httpx → httpcore → anyio идут через event-loop.getaddrinfo, а НЕ socket)    async_orig = asyncio.base_events.BaseEventLoop.getaddrinfo    async def _v4_async(self: Any, host: Any, port: Any = 0, *a: Any, **k: Any) -> Any:        if host in _FORCED_HOSTS:            return [                (socket.AF_INET, socket.SOCK_STREAM, 6, "", (ip, port))                for ip in _TELEGRAM_API_IPS            ]        return await async_orig(self, host, port, *a, **k)    asyncio.base_events.BaseEventLoop.getaddrinfo = _v4_async  # type: ignore[method-assign]

Но и этого не хватило: httpx через httpcore использует собственный resolution, который патчи на getaddrinfo игнорировал. Пришлось спуститься на слой ниже и подменить сетевой бэкенд httpcore, чтобы для api.telegram.org TCP-соединение шло на рабочий IP, а SNI и проверка сертификата оставались по исходному имени хоста — то есть TLS остаётся валидным, ничего не отключаем:

import httpx
from httpcore._backends.auto import AutoBackend
from telegram.request import HTTPXRequest
def _make_telegram_request() -> HTTPXRequest:    class _HardcodedIPBackend(AutoBackend):        async def connect_tcp(self, host: Any, port: Any, *a: Any, **k: Any) -> Any:            if host in _FORCED_HOSTS:                # подменяем только TCP-адрес; SNI/сертификат уровнем выше                # остаются api.telegram.org → соединение валидно и безопасно                host = _TELEGRAM_API_IPS[0]            return await super().connect_tcp(host, port, *a, **k)    transport = httpx.AsyncHTTPTransport()    transport._pool._network_backend = _HardcodedIPBackend()    return HTTPXRequest(...)  # передаём этот transport в бот

Главный вывод: у httpx → httpcore → anyio несколько уровней резолва, и патч на socket.getaddrinfo работает не везде. Для anyio-пути пришлось переопределять именно connect_tcp у бэкенда httpcore.

Грабля №3 — у Telegram несколько дата-центров, и не все доступны с хостинга

Когда я только применил патч, в списке было три IP (три дата-центра Telegram). Бот всё равно висел: ss -tnp показывал SYN-SENT к одному из адресов, а SYN-ACK не возвращался. Я проверил каждый IP с прода curl-ом — отвечал ровно один. На остальные у провайдера, видимо, асимметричная маршрутизация.

При этом httpx выбирал адрес из списка не по порядку, а как придётся, и регулярно попадал на «мёртвый» → connect-timeout → polling стоял. Я оставил один рабочий IP — бот ожил. Этот вечер стоил мне нескольких часов.

Грабля №4 — post_init через присваивание атрибута молча игнорируется

В python-telegram-bot v21 я повесил коллбэк на старт приложения через присваивание:

application = Application.builder().token(TOKEN).build()
application.post_init = _post_init   # ← молча игнорируется

Ни предупреждения, ни ошибки — просто setMyCommands не вызывался при старте, и команды бота не появлялись. Правильно — через билдер:

application = (    Application.builder().token(TOKEN).post_init(_post_init).build()
)

Час дебага, пока не открыл исходники библиотеки. Мораль простая: у билдеров такого рода свойства обычно надо задавать до build(), а не после.

Биллинг на Telegram Stars: single-merchant и entitlement

Самозанятому без юрлица в России удобнее всего принимать платежи через Telegram Stars: не нужны ни ОКВЭД, ни расчётный счёт. Но у этого канала есть неочевидное ограничение, в которое я уперся, когда захотел сделать что-то вроде маркетплейса.

Stars — это single-merchant. Платёж всегда зачисляется на баланс вашего бота и выдаёт «привилегию» именно плательщику. Вы не можете принять деньги покупателя и переслать их стороннему продавцу — для этого нужен лицензированный платёжный агент. Поэтому любая идея «площадки», где платят одни, а получают другие, отпадает сразу: код биллинга физически так не умеет. Остаётся честная модель — вы продаёте свой цифровой продукт конечному пользователю.

Чтобы продавать несколько продуктов независимо, я завёл единый платёжный модуль и обобщённый механизм прав доступа — entitlement. У каждого продукта в каталоге есть поле: что покупка выдаёт. Глобальный тариф (pro) трогает общий флаг пользователя; а для отдельной вертикали (например, «ПДД Pro») выдаётся именно её право — pdd_pro, не задевая остальные продукты:

@dataclass(frozen=True)
class Product:    code: str    title: str    stars_amount: int    duration_months: int | None          # None = бессрочно    grants_tier: str | None = None        # глобальный тариф (Doday Pro)    grants_entitlement: str | None = None # право на одну вертикаль (pdd_pro)

А применение успешного платежа разветвляется по тому, что именно куплено. Существующие тарифные продукты ведут себя как раньше, а entitlement-продукты выдают своё право, не трогая глобальный тариф:

async def apply_successful_payment(session, *, payload, ...) -> None:    product = get_product(...)    # тарифные продукты (Doday Pro) — продлевают глобальный pro    if product.grants_tier is not None:        user.tier = product.grants_tier        user.pro_until = _extend(user.pro_until, product.duration_months)    # entitlement-продукты (ПДД) — выдают право на одну вертикаль,    # не задевая глобальный тариф    if product.grants_entitlement is not None:        await _grant_entitlement(session, user.id, product)

Так «ПДД Pro» можно продавать и оценивать отдельно: своя цена, своя воронка, и при этом покупка Doday Pro не открывает ПДД и наоборот. Добавление новой платной вертикали в будущем — это новая строка в каталоге, а не переписывание биллинга.

SEO как мотор роста

Два продукта — Razbery и Doday ПДД — устроены вокруг компаундящегося контента. Идея простая: бесплатная, открытая и индексируемая часть отвечает на вечные поисковые запросы и приводит органический трафик годами, а монетизация живёт в приватном слое инструментов подготовки.

Razbery, школьное Q&A с разборами

Razbery, школьное Q&A с разборами

Для ПДД я взял официальный набор экзаменационных билетов ГИБДД (открытый материал, который воспроизводят все ПДД-сервисы), нормализовал его в свою схему и засеял 1600 вопросов с иллюстрациями по двум категориям. Каждый вопрос — отдельная индексируемая страница; на каждой странице вопроса отдаётся разметка schema.org/Question, чтобы поисковики показывали ответ в выдаче. sitemap.xml собирается из базы и охватывает обе категории.

хаб Doday ПДД с выбором категории

хаб Doday ПДД с выбором категории

билет ПДД на мобильном: реальные вопросы с фото дорожных ситуаций

билет ПДД на мобильном: реальные вопросы с фото дорожных ситуаций

Поверх бесплатного контента — платный слой за Stars: персональный тренажёр ошибок, симулятор экзамена по официальным правилам, статистика слабых тем. Бесплатная часть остаётся открытой и индексируемой — именно она и есть двигатель привлечения.

Lessio, публичная страница для репетиторов

Lessio, публичная страница для репетиторов

Дев-процесс

Качество я держу инструментами, а не силой воли:

  • mypy --strict на всём app/ — типы обязательны, Any точечно и осознанно.

  • ruff (правила E, F, I, UP, B, S, A, RUF) + форматтер.

  • Свой линтер шаблонов: ловит, например, опасные кавычки в x-data Alpine и слишком мелкий текст.

  • pre-commit hook: формат + линт + типы + линтер шаблонов. Красный коммит не уходит в master.

  • CI на GitHub Actions: тесты и линт на каждый push.

  • Деплой — cron-poll раз в минуту делает git reset --hard origin/master, ставит зависимости, прогоняет миграции Alembic и перезапускает uvicorn. После git push прод обновляется примерно за минуту, без ручных шагов.

Отдельно про работу в паре с AI-агентом, раз уж я её не скрываю. Логика такая: я формулирую, какую фичу хочу; агент читает кодовую базу, предлагает дизайн; после моего «ок» — пишет код, прогоняет тесты, чинит ошибки и коммитит. Звучит как «всё сделал AI», но на практике важнее другое:

  • Решения принимаю я — какие фичи, в каком порядке, на каком стеке, какой UX. Агент предлагает варианты, я выбираю и останавливаю, если что-то идёт не по плану.

  • Я читаю каждый дифф — сначала изменения, потом тесты, потом ручная проверка в браузере.

  • Архитектура — моя — структура по фиче, mypy --strict, отказ от React, отдельный entitlement для биллинга. Это решения, которые агент исполняет.

  • Грабли всё равно ловить руками. Все четыре истории выше — это часы, проведённые в ss -tnp, journalctl и curl --resolve; и только потом — «вот фикс, примени».

Навык, который реально прокачивается, — это не «писать for i in range(n) по памяти», а декомпозировать задачу так, чтобы её можно было исполнить и проверить. Синтаксис я не запоминаю; я запоминаю архитектурные решения и читаю код, который попадает в проект.

Цифры

  • ~76 000 строк: ~33 000 Python в app/, ~25 000 Jinja-шаблонов, ~19 000 в тестах.

  • 39 модулей в app/ — от auth до pdd.

  • 1200+ тестов, зелёных на каждом push (GitHub Actions).

  • 50 роутеров, смонтированных в одном приложении; 49 миграций Alembic.

  • 5 продуктов под одним доменом, одна база, один бот.

  • Деплой ~60 секунд после git push; mypy --strict + ruff + линтер шаблонов в pre-commit, без исключений.

Что дальше

  • Парные задачи и делегирование внутри проектов — для команд из 2–3 человек.

  • Платный слой ПДД и Lessio: первый реальный сквозной платёж через Stars как проверка гипотезы «за это платят».

  • Возможно — десктоп через Tauri на той же кодовой базе: HTMX и Tailwind отлично рендерятся в webview.

Если хотите делать своё

  • HTMX даёт ощущение SPA без бандла. Server-rendered HTML + hx-swap на мобильных оказались быстрее, чем часть React-приложений, что я видел. Подходит не для всего, но для CRUD-интерфейсов — почти идеально.

  • monkey-patch DNS решаем, но нужно понимать слои. У httpx → httpcore → anyio разный resolution; патч на socket.getaddrinfo работает не везде, для anyio придётся переопределять connect_tcp у бэкенда httpcore.

  • themeParams в Telegram Mini App нельзя копировать вслепую. В светлой теме пользователя вы получите белый фон под палитру, рассчитанную на тёмный. Надёжнее — фиксированная тема плюс явный переключатель.

  • Telegram Stars — single-merchant. Если строите что-то платное, сразу заложите, что деньги идут на баланс вашего бота и вы продаёте свой продукт; маркетплейс «одни платят, другие получают» так не сделать.

Если хотите задать вопрос — пишите в комментариях или в issues на GitHub.

Код проекта на GitHub (MIT)

Спасибо, что дочитали.